codewithrocky
v1.0.4
Published
CLI to scaffold production-ready Express + TypeScript + MongoDB backends with RBAC, JWT auth, refresh tokens, and CRUD generators.
Downloads
45
Maintainers
Readme
- Full auth system — registration, login, OTP, password reset, refresh tokens (one command)
- 3 login types — email/password, Google OAuth, Apple Sign In
- Role-based access control — 3 default roles, field-level permissions, scope-based filtering (built in)
- CRUD generator — 12 API endpoints per module with search, filter, sort, pagination (one command)
npx codewithrockyTable of Contents
- What You Get
- Quick Start
- Key Terms
- Login Types
- Social Login Setup
- CLI Commands
- Auth System (11 Endpoints)
- Refresh Tokens
- Account Status & Lifecycle
- RBAC — Role-Based Access Control
- CRUD Generator (12 Endpoints per Module)
- Profile System (7 Endpoints)
- Customization Guide
- Configuration
- Project Structure
- Docker
- Services
- Token Expiry by Role
- Status Codes & Error Handling
- Security Notes
- Environment Variables
- FAQ & Troubleshooting
- License
What You Get
| Feature | What It Does | |---------|-------------| | Auth System | Register, login, OTP verification, password reset — 11 endpoints | | 3 Login Types | Email/password, Google OAuth, Apple Sign In — each with different flows | | RBAC | Super admin, admin, user — with field-level and scope-level permissions | | CRUD Generator | Generate a full module (model + controller + routes) — 12 endpoints each | | Refresh Tokens | Automatic rotation, theft detection, multi-device session management | | Profile Management | Update profile, change password, change email, deactivate/delete account | | Docker | Dev mode with hot reload, production mode, one-command cleanup | | Email Service | SMTP-based OTP delivery with graceful fallback | | Cron Service | Automatic cleanup of expired OTPs and refresh tokens |
Zero config. Zero boilerplate. From install to first API call in under 2 minutes.
Quick Start
Prerequisites
Step 1: Create a project
npx codewithrockyThe interactive CLI starts. Type create:
codewithrocky > createYou'll be asked for a project name:
? Project name: my-api
✔ Project created at ./my-api
✔ Dependencies installedStep 2: Add the auth system
codewithrocky > add auth
✔ Auth system installed (11 endpoints)
✔ RBAC system installed
✔ Profile system installed (7 endpoints)This installs authentication, refresh tokens, RBAC roles, and profile management in one command.
Step 3: Generate a CRUD module
codewithrocky > add crud productsThe interactive builder walks you through your schema:
? Add a field: title
? Field type: String
? Required? Yes
? Add another field? Yes
? Add a field: price
? Field type: Decimal128
? Required? Yes
? Add another field? Yes
? Add a field: category
? Field type: Enum
? Enum values (comma-separated): electronics,clothing,food
? Required? Yes
? Add another field? No
✔ CRUD module "products" created (12 endpoints)Step 4: Start the server
codewithrocky > startOr with Docker:
codewithrocky > localStep 5: Make your first API call
You can test the API using curl (terminal), Postman, Thunder Client (VS Code extension), or any HTTP client.
Register a user:
curl -X POST http://localhost:3100/api/auth/register \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"password": "mypassword123",
"login_type": "manual_login",
"first_name": "John",
"last_name": "Doe"
}'Response:
{
"status": 1,
"message": "OTP sent",
"data": {
"email": "[email protected]"
}
}Where's the OTP? If you haven't configured SMTP email, the OTP is printed in your terminal/server console. Look for a line like
[OTP] [email protected] → 482917. Use that code in the next step.
Confirm the OTP:
curl -X POST http://localhost:3100/api/auth/confirm-otp \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"otp": "482917",
"password": "mypassword123",
"first_name": "John",
"last_name": "Doe"
}'Response:
{
"status": 1,
"message": "Account created",
"data": {
"user_id": "6650a1b2c3d4e5f6a7b8c9d0",
"token": "eyJhbGciOiJIUzI1NiIs...",
"refresh_token": "d8f2a1b4e6c9...",
"role": "super_admin",
"roles": ["super_admin"],
"email": "[email protected]",
"created_at": "2026-03-19T10:30:00.000Z"
}
}Save the
tokenvalue. You'll need it for every other API call. Send it in theAuthorizationheader like this:Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
First user = super_admin. The very first user to register automatically gets the
super_adminrole. All subsequent users get theuserrole.
Create a product (using your token):
curl -X POST http://localhost:3100/api/products/create \
-H "Content-Type: application/json" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
-d '{
"title": "Wireless Headphones",
"price": "79.99",
"category": "electronics"
}'That's it. You have a working API with auth, RBAC, and CRUD — ready for production.
Key Terms
| Term | What It Means |
|------|--------------|
| JWT | JSON Web Token — a signed string that proves who you are. Sent in every API request as Authorization: Bearer <token> |
| OTP | One-Time Password — a 6-digit code sent to your email to verify your identity |
| RBAC | Role-Based Access Control — different users have different permissions based on their role |
| SMTP | Simple Mail Transfer Protocol — the standard for sending emails. Configure it in .env to send real OTP emails |
| Refresh Token | A long-lived token used to get a new access token when the old one expires, without logging in again |
Login Types
The auth system supports 3 login types. Each has a different flow and requires different fields.
Overview
| Login Type | Required Fields | Flow | OTP Required? |
|-----------|----------------|------|--------------|
| manual_login | email, password | Register → OTP → Confirm OTP → Logged in | Yes |
| google_login | social_account_id, platform | Register → Logged in immediately | No |
| apple_login | social_account_id, platform | Register → Logged in immediately | No |
Manual Login (email + password)
This is the default. The user registers with email and password, receives an OTP for verification, then confirms it.
Register (email + password)
↓
OTP sent to email (check terminal if SMTP not configured)
↓
Confirm OTP (email + otp + password)
↓
Account created → access_token + refresh_token returnedGoogle Login
The user signs in with Google on your frontend. Your frontend gets an ID token from Google's SDK. You send that token to your backend.
Frontend: User signs in with Google → gets ID token
↓
Backend: POST /register or /login with social_account_id = ID token
↓
Account created immediately → access_token + refresh_token returnedRequired fields:
login_type:"google_login"social_account_id: the ID token from Google Sign-In SDKplatform:"web","android", or"ios"(must match your Google Client ID)
Apple Login
The user signs in with Apple on your frontend. Your frontend gets an identity token from Apple Sign In SDK. You send that token to your backend.
Frontend: User signs in with Apple → gets identity token
↓
Backend: POST /register or /login with social_account_id = identity token
↓
Account created immediately → access_token + refresh_token returnedRequired fields:
login_type:"apple_login"social_account_id: the identity token from Apple Sign In SDKplatform:"ios"(Apple Sign In is iOS only)
What social_account_id Actually Is
This is not a user ID you create. It's a token that your frontend gets from the Google/Apple SDK after the user signs in on the client side. Your backend verifies this token with Google/Apple servers to confirm the user's identity.
| Login Type | What social_account_id Contains |
|---|---|
| Google (web) | OAuth2 access token from Google Sign-In |
| Google (android/ios) | ID token (JWT) from Google Sign-In |
| Apple | Identity token (JWT) from Apple Sign In |
What platform Does
The platform field tells the backend which Google Client ID to verify against. Each platform (web, android, ios) uses a different Client ID in the Google Developer Console.
For Apple, always use "ios".
Social Login Setup
Google Login — Step by Step
Step 1: Enable in config
Open src/utility/local/config/auth_config.ts and set:
social_login: {
google: { enabled: true }, // ← change from false to true
}Step 2: Set environment variables
Add your Google Client IDs to .env:
GOOGLE_CLIENT_ID_WEB=your-google-client-id-for-web.apps.googleusercontent.com
GOOGLE_CLIENT_ID_ANDROID=your-google-client-id-for-android.apps.googleusercontent.com
GOOGLE_CLIENT_ID_IOS=your-google-client-id-for-ios.apps.googleusercontent.comGet these from Google Cloud Console → APIs & Services → Credentials → OAuth 2.0 Client IDs.
Step 3: Frontend integration
Your frontend app uses the Google Sign-In SDK to let the user sign in. After sign-in, the SDK gives you a token.
Step 4: Call the API
Register:
curl -X POST http://localhost:3100/api/auth/register \
-H "Content-Type: application/json" \
-d '{
"login_type": "google_login",
"social_account_id": "eyJhbGciOiJSUzI1NiIs...",
"platform": "web"
}'Login (for returning users):
curl -X POST http://localhost:3100/api/auth/login \
-H "Content-Type: application/json" \
-d '{
"login_type": "google_login",
"social_account_id": "eyJhbGciOiJSUzI1NiIs...",
"platform": "web"
}'Step 5: Restart your server for config changes to take effect.
Apple Login — Step by Step
Step 1: Enable in config
social_login: {
apple: { enabled: true }, // ← change from false to true
}Step 2: No environment variables needed
Apple Sign In uses public JWKS keys from Apple's servers. No client IDs required on the backend.
Step 3: Frontend integration
Your iOS app uses Apple Sign In SDK. After sign-in, the SDK gives you an identity token.
Step 4: Call the API
curl -X POST http://localhost:3100/api/auth/register \
-H "Content-Type: application/json" \
-d '{
"login_type": "apple_login",
"social_account_id": "eyJhbGciOiJSUzI1NiIs...",
"platform": "ios",
"first_name": "John",
"last_name": "Doe"
}'Note: Apple only sends the user's name on the first sign-in. If you don't capture
first_nameandlast_namein the register call, they'll be empty.
Step 5: Restart your server for config changes to take effect.
Social Login Not Working? Checklist
If you're passing the right params but social login isn't working, check these in order:
| # | Check | How to Fix |
|---|-------|-----------|
| 1 | Is it enabled in config? | Open auth_config.ts → social_login.google.enabled must be true |
| 2 | Are env vars set? (Google only) | .env must have GOOGLE_CLIENT_ID_WEB (or ANDROID/IOS depending on platform) |
| 3 | Is social_account_id in your request? | This is the token from Google/Apple SDK — not an email or user ID |
| 4 | Is platform in your request? | Must be "web", "android", or "ios" |
| 5 | Does platform match your Client ID? | If you're testing from web, use GOOGLE_CLIENT_ID_WEB and platform: "web" |
| 6 | Is the token expired? | Google/Apple tokens are short-lived. Get a fresh one from the frontend |
| 7 | Did you restart the server after config changes? | Config is loaded on startup. Changes need a restart |
CLI Commands
The CLI is interactive. Run npx codewithrocky and type commands inside the shell.
Project Commands
| Command | Description |
|---------|-------------|
| create | Scaffold a new Express + TypeScript + MongoDB project |
| doctor | Check system dependencies (Node, MongoDB, Docker) |
| status | Show which features are installed in the current project |
| help | Show all available commands |
Feature Commands
| Command | Description |
|---------|-------------|
| add auth | Install auth system (11 endpoints + RBAC + refresh tokens + profile) |
| add crud <name> | Generate a CRUD module (12 endpoints) |
Note:
add authinstalls the profile system automatically. You don't need a separate command for profile.
Run Commands
| Command | Description |
|---------|-------------|
| start | Start the project (non-Docker) |
| local | Start in Docker dev mode (hot reload, volumes mounted) |
| server | Start in Docker production mode |
| stop | Stop the running project / containers |
| build | Build TypeScript to JavaScript |
Help Guides
| Command | Description |
|---------|-------------|
| crud help | CRUD generator reference guide |
| auth help | Auth system reference guide |
| rbac help | RBAC system reference guide |
npm Scripts (run from terminal, not the CLI)
These are available as npm scripts in your generated project:
| Script | Description |
|--------|-------------|
| npm run server:detach | Start Docker production in background |
| npm run docker:build | Build Docker image only |
| npm run docker:clean | Remove Docker cache + volumes |
Auth System
The auth system gives you 11 endpoints out of the box. All endpoints are at /api/auth/*.
Flow Overview
Manual Login (email + password):
Register → OTP sent → Confirm OTP → Logged in (token + refresh_token)
↓
Use access_token for API calls
↓
Token expires → Use refresh_token → New tokensSocial Login (Google / Apple):
Register/Login → Logged in immediately (token + refresh_token)
↓
Use access_token for API calls
↓
Token expires → Use refresh_token → New tokensRegister a new user. The flow differs based on login_type.
Manual Login — sends OTP to email:
curl -X POST http://localhost:3100/api/auth/register \
-H "Content-Type: application/json" \
-d '{
"login_type": "manual_login",
"email": "[email protected]",
"password": "mypassword123",
"first_name": "John",
"last_name": "Doe"
}'Google Login — creates account immediately:
curl -X POST http://localhost:3100/api/auth/register \
-H "Content-Type: application/json" \
-d '{
"login_type": "google_login",
"social_account_id": "eyJhbGciOiJSUzI1NiIs...",
"platform": "web"
}'Apple Login — creates account immediately:
curl -X POST http://localhost:3100/api/auth/register \
-H "Content-Type: application/json" \
-d '{
"login_type": "apple_login",
"social_account_id": "eyJhbGciOiJSUzI1NiIs...",
"platform": "ios",
"first_name": "John",
"last_name": "Doe"
}'Request Body:
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| login_type | string | Yes | "manual_login", "google_login", or "apple_login" |
| email | string | Manual only | User's email address |
| password | string | Manual only | Minimum 6 characters (configurable) |
| social_account_id | string | Social only | ID token from Google/Apple SDK |
| platform | string | Social only | "web", "android", or "ios" |
| first_name | string | No | User's first name |
| last_name | string | No | User's last name |
| email_notifications | boolean | No | Enable email notifications (default: true) |
| time_zone | string | No | IANA timezone string (e.g., "America/New_York") |
Response — Manual Login (200):
{
"status": 1,
"message": "OTP sent",
"data": {
"email": "[email protected]"
}
}After this, call
/confirm-otpwith the OTP code. Check your terminal for the OTP if SMTP is not configured.
Response — Social Login (200):
{
"status": 1,
"message": "Account created",
"data": {
"user_id": "6650a1b2c3d4e5f6a7b8c9d0",
"first_name": "John",
"last_name": "Doe",
"email": "[email protected]",
"login_type": "google_login",
"token": "eyJhbGciOiJIUzI1NiIs...",
"refresh_token": "d8f2a1b4e6c9...",
"role": "user",
"roles": ["user"],
"created_at": "2026-03-19T10:30:00.000Z"
}
}Note: The first user to register (any login type) becomes the
super_admin. All subsequent users get theuserrole.
Confirm the OTP sent during manual registration. This completes the registration and logs the user in.
This endpoint is only for
manual_login. Social login users are logged in immediately on register — they don't need OTP confirmation.
Request:
curl -X POST http://localhost:3100/api/auth/confirm-otp \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"otp": "482917",
"password": "mypassword123",
"first_name": "John",
"last_name": "Doe"
}'Request Body:
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| email | string | Yes | Same email used in register |
| otp | string | Yes | 6-digit OTP from email (or terminal) |
| password | string | Yes | Same password used in register |
| first_name | string | No | User's first name |
| last_name | string | No | User's last name |
| email_notifications | boolean | No | Enable email notifications (default: true) |
| time_zone | string | No | IANA timezone string |
Response (200):
{
"status": 1,
"message": "Account created",
"data": {
"user_id": "6650a1b2c3d4e5f6a7b8c9d0",
"token": "eyJhbGciOiJIUzI1NiIs...",
"refresh_token": "d8f2a1b4e6c9...",
"role": "user",
"roles": ["user"],
"email": "[email protected]",
"created_at": "2026-03-19T10:30:00.000Z"
}
}Log in with an existing account. Works with all 3 login types.
Manual Login:
curl -X POST http://localhost:3100/api/auth/login \
-H "Content-Type: application/json" \
-d '{
"login_type": "manual_login",
"email": "[email protected]",
"password": "mypassword123"
}'Google Login:
curl -X POST http://localhost:3100/api/auth/login \
-H "Content-Type: application/json" \
-d '{
"login_type": "google_login",
"social_account_id": "eyJhbGciOiJSUzI1NiIs...",
"platform": "web"
}'Apple Login:
curl -X POST http://localhost:3100/api/auth/login \
-H "Content-Type: application/json" \
-d '{
"login_type": "apple_login",
"social_account_id": "eyJhbGciOiJSUzI1NiIs...",
"platform": "ios"
}'Request Body:
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| login_type | string | Yes | "manual_login", "google_login", or "apple_login" |
| email | string | Manual only | Registered email |
| password | string | Manual only | Account password |
| social_account_id | string | Social only | Fresh ID token from Google/Apple SDK |
| platform | string | Social only | "web", "android", or "ios" |
Response (200):
{
"status": 1,
"message": "Login successful",
"data": {
"user_id": "6650a1b2c3d4e5f6a7b8c9d0",
"first_name": "John",
"last_name": "Doe",
"email": "[email protected]",
"login_type": "manual_login",
"token": "eyJhbGciOiJIUzI1NiIs...",
"refresh_token": "d8f2a1b4e6c9...",
"role": "user",
"roles": ["user"],
"created_at": "2026-03-19T10:30:00.000Z"
}
}Blocked account responses:
If the account is suspended or deleted, login returns a special status code instead of the normal response. See Account Status & Lifecycle and Status Codes for details.
Send an OTP for password reset.
Manual login only. Social login users (Google/Apple) don't have passwords — they authenticate through their provider. If a social login user calls this endpoint, it returns an error.
Request:
curl -X POST http://localhost:3100/api/auth/send-otp \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"auth_type": "forget_password"
}'Request Body:
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| email | string | Yes | Registered email |
| auth_type | string | Yes | Must be "forget_password" |
Response (200):
{
"status": 1,
"message": "OTP sent",
"data": {
"email": "[email protected]"
}
}Verify the OTP sent for password reset. This does not reset the password — it confirms the OTP is valid.
Request:
curl -X POST http://localhost:3100/api/auth/verify-forget-password-otp \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"otp": "593817"
}'Request Body:
| Field | Type | Required | Description | |-------|------|----------|-------------| | email | string | Yes | Email that received the OTP | | otp | string | Yes | 6-digit OTP from email |
Response (200):
{
"status": 1,
"message": "OTP verified",
"data": {
"email": "[email protected]",
"otp_verified": true
}
}Reset the password after OTP verification. This kills all existing refresh tokens for the user (logged out on every device).
Manual login only. Social login users cannot reset passwords.
Request:
curl -X POST http://localhost:3100/api/auth/reset-password \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"otp": "593817",
"new_password": "newsecurepassword",
"confirm_password": "newsecurepassword"
}'Request Body:
| Field | Type | Required | Description | |-------|------|----------|-------------| | email | string | Yes | Registered email | | otp | string | Yes | Verified OTP | | new_password | string | Yes | New password (min 6 chars) | | confirm_password | string | Yes | Must match new_password |
Response (200):
{
"status": 1,
"message": "Password reset",
"data": null
}Security: Password reset kills ALL refresh tokens. The user must log in again on every device.
Check if a user exists. Useful for pre-validation before registration. Works with all 3 login types.
Manual Login:
curl -X POST http://localhost:3100/api/auth/check-user-exists \
-H "Content-Type: application/json" \
-d '{
"login_type": "manual_login",
"email": "[email protected]"
}'Google/Apple Login:
curl -X POST http://localhost:3100/api/auth/check-user-exists \
-H "Content-Type: application/json" \
-d '{
"login_type": "google_login",
"social_account_id": "eyJhbGciOiJSUzI1NiIs...",
"platform": "web"
}'Request Body:
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| login_type | string | Yes | "manual_login", "google_login", or "apple_login" |
| email | string | Manual only | Email to check |
| social_account_id | string | Social only | ID token from Google/Apple SDK |
| platform | string | Social only | "web", "android", or "ios" |
Response — User Found (200):
{
"status": 1,
"message": "User found",
"data": {
"user_exists": true,
"user_id": "6650a1b2c3d4e5f6a7b8c9d0",
"first_name": "John",
"last_name": "Doe",
"email": "[email protected]",
"login_type": "manual_login"
}
}Response — User Not Found (200):
{
"status": 1,
"message": "User not found",
"data": {
"user_exists": false
}
}Get a new access token using a refresh token. No authorization header needed.
Request:
curl -X POST http://localhost:3100/api/auth/refresh-token \
-H "Content-Type: application/json" \
-d '{
"refresh_token": "d8f2a1b4e6c9..."
}'Request Body:
| Field | Type | Required | Description | |-------|------|----------|-------------| | refresh_token | string | Yes | Valid refresh token from login |
Response (200):
{
"status": 1,
"message": "Token refreshed",
"data": {
"token": "eyJhbGciOiJIUzI1NiIs...",
"refresh_token": "a1b2c3d4e5f6...",
"user_id": "6650a1b2c3d4e5f6a7b8c9d0",
"first_name": "John",
"last_name": "Doe",
"email": "[email protected]",
"role": "user",
"roles": ["user"]
}
}Important: Each refresh token can only be used once. After use, a new refresh token is returned. Save it — the old one is dead.
Error Responses:
| Status | error_code | Meaning |
|--------|-----------|---------|
| 401 | REFRESH_TOKEN_INVALID | Token not found, expired, or invalid |
| 401 | TOKEN_REUSE_DETECTED | This token was already used — possible theft. All sessions for this device are killed. Log in again. |
Verify that an access token is still valid. Returns the user's basic info. Useful for auto-login on app start.
Request:
curl -X POST http://localhost:3100/api/auth/verify-token \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."Headers:
| Header | Value | Required |
|--------|-------|----------|
| Authorization | Bearer {access_token} | Yes |
Response (200):
{
"status": 1,
"message": "Token valid",
"data": {
"user_id": "6650a1b2c3d4e5f6a7b8c9d0",
"first_name": "John",
"last_name": "Doe",
"email": "[email protected]"
}
}Log out the current session. Optionally pass the refresh token to kill it.
Request:
curl -X POST http://localhost:3100/api/auth/logout \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
-H "Content-Type: application/json" \
-d '{
"refresh_token": "d8f2a1b4e6c9..."
}'Headers:
| Header | Value | Required |
|--------|-------|----------|
| Authorization | Bearer {access_token} | Yes |
Request Body:
| Field | Type | Required | Description | |-------|------|----------|-------------| | refresh_token | string | No | Kills this specific refresh token |
Response (200):
{
"status": 1,
"message": "Logged out",
"data": null
}Log out from ALL devices. Kills every refresh token for this user.
Request:
curl -X POST http://localhost:3100/api/auth/logout-all \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."Headers:
| Header | Value | Required |
|--------|-------|----------|
| Authorization | Bearer {access_token} | Yes |
Response (200):
{
"status": 1,
"message": "All sessions ended",
"data": null
}Refresh Tokens
How It Works
Refresh tokens solve a common problem: you want short-lived access tokens (for security) but you don't want users to log in every hour.
Here's the flow:
1. Login
└── You get: access_token (short-lived) + refresh_token (long-lived)
2. Use access_token
└── Send it in the Authorization header for every API call
3. Access token expires
└── POST /api/auth/refresh-token with your refresh_token
4. Get new tokens
└── New access_token + NEW refresh_token (old one is destroyed)
5. Repeat from step 2Theft Detection
Every refresh token can only be used once. When you use it, you get a new one. The old one dies immediately.
If an attacker steals your refresh token and uses it before you do:
- They get new tokens
- When YOU try to use the old refresh token, it fails
- The system detects this as theft and kills ALL tokens for that session
- Both you and the attacker are logged out
This is called refresh token rotation with reuse detection.
How Refresh Tokens Are Stored
Refresh tokens are never stored in plain text. The backend stores a bcrypt hash. Even if the database is compromised, the tokens can't be extracted.
Each token has:
token_id— first 16 characters, used for fast database lookuptoken_hash— bcrypt hash of the full tokenfamily_id— groups tokens from the same login session (for theft detection)device_info— from the User-Agent header (for multi-device tracking)
Multi-Device Sessions
Each role has a maximum number of simultaneous sessions:
| Role | Max Sessions | |------|-------------| | super_admin | 1 | | admin | 3 | | user | 5 |
If a user logs in on a 6th device, the oldest session is automatically killed.
What Kills Refresh Tokens
| Action | Effect | |--------|--------| | Logout | Kills the specific refresh token you pass | | Logout All | Kills ALL refresh tokens for the user | | Password Reset | Kills ALL refresh tokens for the user | | Password Change (profile) | Kills ALL refresh tokens for the user | | Token Reuse Detected | Kills ALL tokens for that session (family) | | Max Sessions Exceeded | Kills the oldest session |
Account Status & Lifecycle
Every user has an account_status that determines whether they can log in and what message they see.
Status Values
| Status | What It Means | Can Log In? | How It's Set |
|--------|--------------|-------------|-------------|
| active | Normal account | Yes | Default for new accounts |
| inactive_by_admin | Suspended by admin | No (status 5) | Admin sets this in database |
| inactive_by_user | Deactivated by user | Yes (reactivates on login) | User calls /profile/deactivate-account |
| deleted_by_user | User deleted their account | No (status 6) | User calls /profile/delete-account |
| deleted_by_admin | Admin deleted the account | No (status 7) | Admin sets this in database |
What the User Sees on Login
When a blocked user tries to log in, they get a special status code:
| Status Code | Message | Support Email Shown? | |------------|---------|---------------------| | 5 | "Your account has been suspended. Contact support for assistance." | Yes (if configured) | | 6 | "This account was previously deleted. You can create a new account." | No | | 7 | "Your account has been removed by admin. Contact support for assistance." | Yes (if configured) | | 10 | "User not found" / "No account linked with this social account" | No |
Configuring the Support Email
When users see status 5 or 7, the response can include a support email. Set it in auth_config.ts:
account: {
blocked_account_support_email: "[email protected]"
}If left empty, no support email is shown in the response.
RBAC — Role-Based Access Control
The Simple Version
Every user has a role. Every role has permissions. Permissions say what the role can do, where, and under what conditions.
Default Roles
| Role | Level | What They Can Do |
|------|-------|-----------------|
| super_admin | 1000 | Everything. Full access to all modules, all data, all fields. |
| admin | 100 | Full CRUD on all modules. Can see all data. |
| user | 1 | Basic CRUD on all modules. Own data only. Active records only. |
Level determines hierarchy. A role can only manage roles with a lower level. A
super_admin(1000) can manageadmin(100), but not the other way around.
Permission Structure
Each permission answers 5 questions:
| Question | Field | Example |
|----------|-------|---------|
| WHO can do this? | role | "admin" |
| WHAT can they do? | actions | { create: true, read: true, update: true, delete: true } |
| WHERE does it apply? | module | "products" or "*" (all modules) |
| WHEN / under what conditions? | scope | { ownership: "all", status: ["*"] } |
| EFFECT — allow or deny? | effect | "allow" or "deny" |
When both
allowanddenyexist for the same action, deny always wins.
How Permissions Are Evaluated
When a request comes in, the RBAC middleware checks in this order:
1. Is there an EXPLICIT DENY for this role + module? → BLOCKED (deny always wins)
2. Is there an EXPLICIT ALLOW for this role + module? → check actions, scope, fields
3. Is there a WILDCARD (*) ALLOW for this role? → check actions, scope, fields
4. Does the role have defaultAccess = true? → use defaultPermissions
5. None of the above? → BLOCKED (default deny)This is the same model as AWS IAM: everything is denied unless explicitly allowed, and any deny overrides any allow.
What the RBAC Middleware Checks (in order)
Request comes in
↓
1. Extract JWT from Authorization header
↓ (fails → 401 "Authorization token is required")
2. Verify JWT signature and expiry
↓ (fails → 401 TOKEN_EXPIRED or INVALID_TOKEN)
3. Find user in database
↓ (fails → 401 USER_NOT_FOUND)
4. Check account_status is active
↓ (fails → 401 ACCOUNT_INACTIVE)
5. Look up user's role
↓ (fails → 403 ROLE_NOT_FOUND)
6. Check role is active
↓ (fails → 403 ROLE_INACTIVE)
7. Resolve permissions for this role + module + action
↓ (fails → 403 ACCESS_DENIED)
8. Apply scope filters (ownership, status, etc.)
↓
9. Apply field filters (prevent/allow)
↓
Request proceeds to controllerScope Options
Scopes let you restrict what data a role can access:
| Scope | Values | Description |
|-------|--------|-------------|
| ownership | "own", "team", "department", "assigned", "all" | Whose data can they see? |
| status | ["active"], ["active", "deleted"], ["*"] | Which record statuses? |
| customFilter | { region: "$user.region" } | Dynamic filter using user's own fields |
| maxRecords | 10, 50, 100 | Cap the number of results returned |
| dateRange | 30 | Only records from the last N days |
Example: A user role with ownership: "own" and status: ["active"] can only see their own active records.
Field-Level Access
Control which fields each role can see or modify:
Prevent mode (blacklist) — block specific fields, everything else allowed:
{
mode: "prevent",
prevent: {
read: ["internal_notes", "cost_price"], // won't appear in responses
create: ["is_featured"], // silently stripped on create
update: ["sku"] // silently stripped on update
}
}Allow mode (whitelist) — only these fields, everything else blocked:
{
mode: "allow",
allow: {
read: ["title", "price", "description", "category"],
create: ["title", "price", "description", "category"]
}
}Hidden field override:
Fields marked as hidden in the CRUD schema are hidden from API responses by default. Roles with canOverrideHidden: true can see them using ?fields=hidden_field.
Managing Roles
How to Create a Custom Role
Step 1: Open src/config/roles.seed.ts
Step 2: Add your role to the seedRoles array:
{
name: "editor",
label: "Content Editor",
level: 50,
is_active: true,
is_system: false,
is_protected: false,
description: "Can read and edit content, cannot delete or manage users",
default_access: false,
default_permissions: {
actions: ["read"],
scope: {
ownership: "all",
status: ["active"],
customFilter: null,
maxRecords: null,
dateRange: null,
},
fields: {
mode: "prevent",
prevent: { read: [], create: [], update: [] },
allow: { read: [], create: [], update: [] },
canOverrideHidden: false,
},
},
},Step 3: Add permissions for each module the role can access. Add to seedPermissions array:
{
role: "editor",
module: "products",
effect: "allow",
actions: {
create: false,
read: true,
update: true,
delete: false,
hardDelete: false,
restore: false,
bulkDelete: false,
bulkUpdate: false,
copy: false,
move: false,
export: true,
import: false,
},
scope: {
ownership: "all",
status: ["active"],
customFilter: null,
maxRecords: 50,
dateRange: null,
},
fields: {
mode: "prevent",
prevent: {
read: ["cost_price", "supplier_id"],
create: [],
update: ["price", "cost_price"],
},
allow: { read: [], create: [], update: [] },
canOverrideHidden: false,
},
is_active: true,
},Step 4: Re-seed the database (see below).
How to Assign a Role to a User
Update the user's roles field in MongoDB:
// In MongoDB shell (mongosh):
use myproject
db.users.updateOne(
{ email: "[email protected]" },
{ $set: { roles: ["editor"] } }
)The first role in the array is the primary role — it determines token expiry and session limits.
How to Re-Seed Roles After Changes
Roles and permissions are seeded into the database on the first server start (when the collections are empty). After that, the database is the source of truth.
To apply changes from roles.seed.ts:
// In MongoDB shell (mongosh):
use myproject
db.roles.drop()
db.permissions.drop()
// Then restart the server — it will re-seed from roles.seed.tsWarning: This deletes any runtime changes you made to roles/permissions directly in the database. Only the seed file survives.
How to Block a Role (Without Deleting It)
Set is_active: false on the role. Users with that role will get a 403 ROLE_INACTIVE error on every request.
Audit Logging
RBAC actions are logged to the audit_logs collection.
Audit levels (set in roles.seed.ts → auditConfig.level):
| Level | What's Logged |
|-------|--------------|
| "none" | Nothing |
| "deny" | Only denied requests (default) |
| "write" | Denied requests + all write operations (create, update, delete) |
| "all" | Every request |
What's logged: user ID, email, role, module, action, effect (allow/deny), reason, IP address, user agent, timestamp.
Sensitive fields are auto-redacted: password, new_password, old_password, token, refresh_token, otp, social_account_id.
Retention: Old audit logs are kept for 90 days by default (configurable in auditConfig.retentionDays).
CRUD Generator
Generating a Module
codewithrocky > add crud productsThe interactive builder asks you for each field:
- Field name — e.g.,
title,price,category - Field type — one of the 11 supported types
- Options — required, unique, default value, min/max, etc.
Supported Field Types
| # | Type | Description | Options |
|---|------|-------------|---------|
| 1 | String | Text field | trim, unique, index, minLength, maxLength |
| 2 | Number | Integer or float | min, max |
| 3 | Boolean | true/false | default |
| 4 | Date | ISO date | — |
| 5 | ObjectId | Reference to another model | ref (model name) |
| 6 | Enum | One of predefined values | values (array) |
| 7 | Array of Strings | List of text values | — |
| 8 | Array of Objects | Nested objects with sub-fields | Sub-field builder |
| 9 | Array of ObjectIds | List of references | ref (model name) |
| 10 | Object/Mixed | Flexible structure | — |
| 11 | Decimal128 | High-precision number (currency) | — |
What Values Can I Send? — By Field Type
When you create or update a record, here's exactly what each field type accepts:
{
"title": "My Product",
"price": 29.99,
"in_stock": true,
"release_date": "2026-06-15T10:00:00Z",
"category_id": "6507f1f77bcf86cd799439011",
"status": "draft",
"tags": ["electronics", "sale", "new"],
"variants": [
{ "color": "red", "size": "M", "qty": 10 },
{ "color": "blue", "size": "L", "qty": 5 }
],
"related_products": [
"6507f1f77bcf86cd799439012",
"6507f1f77bcf86cd799439013"
],
"metadata": { "weight": "2kg", "material": "cotton" },
"exact_price": "29.99"
}What you CANNOT send (auto-managed by the system):
_id— auto-generatedcreated_by— set from your JWT tokencreated_at— auto-set to current timeupdated_at— auto-set on every updatedocument_entry_status— managed by delete/restore__v— Mongoose version key- Fields marked as
createOnly— can't change after creation - Fields marked as
updateOnly— can't set during creation - Fields marked as
systemManaged— you can never set these
What happens with wrong values:
- Missing required field →
"Field 'title' is required" - Invalid ObjectId →
"Invalid ID format for 'category_id'" - Wrong enum value →
"'invalid' is not a valid value for 'status'" - Duplicate unique field →
"A record with this title already exists" - Extra unknown fields → silently ignored (no error)
Generated Endpoints
Every CRUD module generates 12 endpoints at /api/{module}/*.
For a module called products, the base URL is /api/products.
Create a new record.
Request:
curl -X POST http://localhost:3100/api/products/create \
-H "Content-Type: application/json" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
-d '{
"title": "Wireless Headphones",
"price": "79.99",
"category": "electronics"
}'Response (200):
{
"status": 1,
"message": "Record created",
"data": {
"_id": "6650b2c3d4e5f6a7b8c9d0e1",
"title": "Wireless Headphones",
"price": "79.99",
"category": "electronics",
"created_by": "6650a1b2c3d4e5f6a7b8c9d0",
"is_deleted": false,
"created_at": "2026-03-19T11:00:00.000Z",
"updated_at": "2026-03-19T11:00:00.000Z"
}
}Get a single record by ID. Use ?fields= to return only specific fields.
Request:
curl -X GET "http://localhost:3100/api/products/get/6650b2c3d4e5f6a7b8c9d0e1" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."With field projection:
curl -X GET "http://localhost:3100/api/products/get/6650b2c3d4e5f6a7b8c9d0e1?fields=title,price" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."Response (200):
{
"status": 1,
"message": "Record found",
"data": {
"_id": "6650b2c3d4e5f6a7b8c9d0e1",
"title": "Wireless Headphones",
"price": "79.99",
"category": "electronics",
"created_by": "6650a1b2c3d4e5f6a7b8c9d0",
"is_deleted": false,
"created_at": "2026-03-19T11:00:00.000Z",
"updated_at": "2026-03-19T11:00:00.000Z"
}
}List records with pagination, search, sorting, and filtering.
Request:
curl -X GET "http://localhost:3100/api/products/list?page=1&limit=20&search=headphones&sort_by=price&sort_order=asc" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."With filters:
curl -X GET "http://localhost:3100/api/products/list?category=electronics&price_min=50&price_max=200" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."Response (200):
{
"status": 1,
"message": "Records found",
"data": {
"docs": [
{
"_id": "6650b2c3d4e5f6a7b8c9d0e1",
"title": "Wireless Headphones",
"price": "79.99",
"category": "electronics",
"created_by": "6650a1b2c3d4e5f6a7b8c9d0",
"created_at": "2026-03-19T11:00:00.000Z"
}
],
"totalDocs": 1,
"limit": 20,
"page": 1,
"totalPages": 1,
"hasNextPage": false,
"hasPrevPage": false
}
}Get the total count of records matching the filters. Accepts the same filters as /list.
Request:
curl -X GET "http://localhost:3100/api/products/count?category=electronics" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."Response (200):
{
"status": 1,
"message": "Count retrieved",
"data": {
"count": 42
}
}Partial update — send only the fields you want to change.
Request:
curl -X POST http://localhost:3100/api/products/update/6650b2c3d4e5f6a7b8c9d0e1 \
-H "Content-Type: application/json" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
-d '{
"price": "69.99"
}'Response (200):
{
"status": 1,
"message": "Record updated",
"data": {
"_id": "6650b2c3d4e5f6a7b8c9d0e1",
"title": "Wireless Headphones",
"price": "69.99",
"category": "electronics",
"created_by": "6650a1b2c3d4e5f6a7b8c9d0",
"is_deleted": false,
"created_at": "2026-03-19T11:00:00.000Z",
"updated_at": "2026-03-19T11:15:00.000Z"
}
}Soft delete — marks the record as deleted but keeps it in the database.
Request:
curl -X DELETE "http://localhost:3100/api/products/delete/6650b2c3d4e5f6a7b8c9d0e1" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."Response (200):
{
"status": 1,
"message": "Record deleted",
"data": {
"_id": "6650b2c3d4e5f6a7b8c9d0e1",
"is_deleted": true
}
}Soft delete multiple records at once.
Request:
curl -X POST http://localhost:3100/api/products/bulk-delete \
-H "Content-Type: application/json" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
-d '{
"ids": [
"6650b2c3d4e5f6a7b8c9d0e1",
"6650b2c3d4e5f6a7b8c9d0e2",
"6650b2c3d4e5f6a7b8c9d0e3"
]
}'Response (200):
{
"status": 1,
"message": "3 records deleted",
"data": {
"deleted_count": 3
}
}Undo a soft delete.
Request:
curl -X POST "http://localhost:3100/api/products/restore/6650b2c3d4e5f6a7b8c9d0e1" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."Response (200):
{
"status": 1,
"message": "Record restored",
"data": {
"_id": "6650b2c3d4e5f6a7b8c9d0e1",
"is_deleted": false
}
}Reverse lookup — find all records that reference a specific document.
For example, find all products created by a specific user:
Request:
curl -X GET "http://localhost:3100/api/products/by/created_by/6650a1b2c3d4e5f6a7b8c9d0" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."Response (200):
{
"status": 1,
"message": "Records found",
"data": [
{
"_id": "6650b2c3d4e5f6a7b8c9d0e1",
"title": "Wireless Headphones",
"price": "79.99",
"category": "electronics",
"created_by": "6650a1b2c3d4e5f6a7b8c9d0"
}
]
}List all reference (ObjectId) fields on this model and what they point to.
Request:
curl -X GET "http://localhost:3100/api/products/relations" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."Response (200):
{
"status": 1,
"message": "Relations found",
"data": [
{
"field": "created_by",
"ref": "users"
}
]
}Schema introspection — returns the full schema definition including field types, validations, and options.
Request:
curl -X GET "http://localhost:3100/api/products/metadata" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."Response (200):
{
"status": 1,
"message": "Metadata retrieved",
"data": {
"model": "products",
"fields": {
"title": { "type": "String", "required": true },
"price": { "type": "Decimal128", "required": true },
"category": { "type": "String", "enum": ["electronics", "clothing", "food"], "required": true },
"created_by": { "type": "ObjectId", "ref": "users" },
"is_deleted": { "type": "Boolean", "default": false }
}
}
}Cross-model discovery — get a record and include related records from other modules.
Request:
curl -X GET "http://localhost:3100/api/products/get/6650b2c3d4e5f6a7b8c9d0e1/related?include=reviews&reviews.status=active" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."Query Parameters:
| Param | Description |
|-------|-------------|
| include | Comma-separated list of related modules to include |
| {module}.{field} | Filter on the included module's fields |
Response (200):
{
"status": 1,
"message": "Record with relations",
"data": {
"_id": "6650b2c3d4e5f6a7b8c9d0e1",
"title": "Wireless Headphones",
"price": "79.99",
"category": "electronics",
"related": {
"reviews": [
{
"_id": "6650c3d4e5f6a7b8c9d0e1f2",
"rating": 5,
"comment": "Great sound quality",
"status": "active"
}
]
}
}
}Query Parameters Reference
Every /list and /count endpoint supports these query parameters:
| Parameter | Example | Description |
|-----------|---------|-------------|
| page | ?page=2 | Page number (default: 1) |
| limit | ?limit=50 | Items per page (default: 20, max: 100) |
| search | ?search=keyword | Full-text search across all string fields |
| sort_by | ?sort_by=price | Field to sort by (default: created_at) |
| sort_order | ?sort_order=asc | asc or desc (default: desc) |
| fields | ?fields=title,price | Return only these fields (projection) |
| {field} | ?status=draft | Exact match filter on any field |
| {field}=a,b | ?status=draft,published | IN filter — matches any of the values |
| {field}_min | ?price_min=10 | Range filter: greater than or equal to |
| {field}_max | ?price_max=100 | Range filter: less than or equal to |
| created_from | ?created_from=2026-01-01 | Records created on or after this date |
| created_to | ?created_to=2026-12-31 | Records created on or before this date |
| {ref}.{field} | [email protected] | Filter on a populated reference field |
Combine everything:
curl -X GET "http://localhost:3100/api/products/list?page=1&limit=10&search=headphones&category=electronics&price_min=50&price_max=200&sort_by=price&sort_order=asc&fields=title,price,category" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."Filter Examples — Every Type Explained
Here's every way you can filter data, with real examples:
1. Exact Match — find records where a field equals a value
# Find all products in the "electronics" category
curl "http://localhost:3100/api/products/list?category=electronics" -H "Authorization: Bearer TOKEN"
# Find completed tasks
curl "http://localhost:3100/api/tasks/list?completed=true" -H "Authorization: Bearer TOKEN"
# Find tasks assigned to a specific user (ObjectId)
curl "http://localhost:3100/api/tasks/list?assigned_to=6507f1f77bcf86cd799439011" -H "Authorization: Bearer TOKEN"2. Multiple Values (IN filter) — find records matching ANY of the values
# Products in electronics OR clothing
curl "http://localhost:3100/api/products/list?category=electronics,clothing" -H "Authorization: Bearer TOKEN"3. Number Range — find records within a min/max range
# Products priced between $10 and $100
curl "http://localhost:3100/api/products/list?price_min=10&price_max=100" -H "Authorization: Bearer TOKEN"
# Products priced $50 or more (no max)
curl "http://localhost:3100/api/products/list?price_min=50" -H "Authorization: Bearer TOKEN"4. Date Range — find records created within a time period
# Products created in January 2026
curl "http://localhost:3100/api/products/list?created_from=2026-01-01&created_to=2026-01-31" -H "Authorization: Bearer TOKEN"5. Text Search — search across ALL string fields
# Search for "wireless" in title, description, category, or any string field
curl "http://localhost:3100/api/products/list?search=wireless" -H "Authorization: Bearer TOKEN"6. Populated Field Filter — filter by a referenced document's field
# Find products created by a user with a specific email
curl "http://localhost:3100/api/products/[email protected]" -H "Authorization: Bearer TOKEN"7. Field Projection — return only specific fields
# Only return title and price (smaller response, faster)
curl "http://localhost:3100/api/products/list?fields=title,price" -H "Authorization: Bearer TOKEN"8. Sorting
# Cheapest first
curl "http://localhost:3100/api/products/list?sort_by=price&sort_order=asc" -H "Authorization: Bearer TOKEN"
# Alphabetical by title
curl "http://localhost:3100/api/products/list?sort_by=title&sort_order=asc" -H "Authorization: Bearer TOKEN"9. Pagination
# First page, 10 items
curl "http://localhost:3100/api/products/list?page=1&limit=10" -H "Authorization: Bearer TOKEN"
# Second page
curl "http://localhost:3100/api/products/list?page=2&limit=10" -H "Authorization: Bearer TOKEN"10. Combine Everything
# Electronics under $200, search "headphone", cheapest first, page 1, only title+price
curl "http://localhost:3100/api/products/list?category=electronics&price_max=200&search=headphone&sort_by=price&sort_order=asc&page=1&limit=5&fields=title,price" \
-H "Authorization: Bearer TOKEN"Tip: All these filters work on
/counttoo — just replace/listwith/countto get the matching count without fetching data.
Profile System
All profile endpoints require authentication. Base URL: /api/profile.
Note: Profile is installed automatically when you run
add auth. You don't need a separate command.
Get the current user's profile.
Request:
curl -X GET http://localhost:3100/api/profile/get \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."Response (200):
{
"status": 1,
"message": "Profile found",
"data": {
"user_id": "6650a1b2c3d4e5f6a7b8c9d0",
"first_name": "John",
"last_name": "Doe",
"email": "[email protected]",
"login_type": "manual_login",
"email_notifications": true,
"time_zone": null,
"profile_image": null,
"created_at": "2026-03-19T10:30:00.000Z"
}
}Update profile fields. Send only the fields you want to change.
Request:
curl -X POST http://localhost:3100/api/profile/update \
-H "Content-Type: application/json" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
-d '{
"first_name": "Jonathan",
"time_zone": "America/New_York",
"email_notifications": false
}'Updatable Fields:
| Field | Type | Description |
|-------|------|-------------|
| first_name | string | First name |
| last_name | string | Last name |
| email_notifications | boolean | Enable/disable email notifications |
| time_zone | string | IANA timezone string |
| profile_image | string or null | URL or null to remove |
Response (200):
{
"status": 1,
"message": "Profile updated",
"data": {
"user_id": "6650a1b2c3d4e5f6a7b8c9d0",
"first_name": "Jonathan",
"last_name": "Doe",
"email": "[email protected]",
"login_type": "manual_login",
"email_notifications": false,
"time_zone": "America/New_York",
"profile_image": null,
"created_at": "2026-03-19T10:30:00.000Z"
}
}Change the account password.
Manual login only. Social login users (Google/Apple) don't have passwords. If a social login user calls this endpoint, it returns:
"Password management is not available for social login accounts".
Request:
curl -X POST http://localhost:3100/api/profile/change-password \
-H "Content-Type: application/json" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
-d '{
"old_password": "mypassword123",
"new_password": "newstrongpassword",
"confirm_password": "newstrongpassword"
}'Request Body:
| Field | Type | Required | Description | |-------|------|----------|-------------| | old_password | string | Yes | Current password | | new_password | string | Yes | New password (min 6 chars) | | confirm_password | string | Yes | Must match new_password |
Response (200):
{
"status": 1,
"message": "Password changed",
"data": null
}Security: All existing sessions are invalidated. The user should log in again with the new password.
Request an email change. Sends an OTP to the new email for verification.
Not available for Google login accounts. Google email is linked to the Google account and cannot be changed independently. Apple login users CAN change their email.
Request:
curl -X POST http://localhost:3100/api/profile/change-email \
-H "Content-Type: application/json" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
-d '{
"new_email": "[email protected]"
}'Response (200):
{
"status": 1,
"message": "OTP sent to new email",
"data": {
"new_email": "[email protected]"
}
}Confirm the email change with the OTP sent to the new email.
Request:
curl -X POST http://localhost:3100/api/profile/verify-email-change \
-H "Content-Type: application/json" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
-d '{
"new_email": "[email protected]",
"otp": "718293"
}'Response (200):
{
"status": 1,
"message": "Email changed",
"data": {
"new_email": "[email protected]"
}
}A notification email is sent to the old email address informing them of the change.
Temporarily deactivate the account. Sets status to inactive_by_user. The user can log in again to reactivate.
Request:
curl -X POST http://localhost:3100/api/profile/deactivate-account \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."Response (200):
{
"status": 1,
"message": "Account deactivated",
"data": null
}Permanently delete the account (soft delete). Sets status to deleted_by_user.
Manual login users must confirm with their password. Social login users can delete without a password.
Request (manual login):
curl -X POST http://localhost:3100/api/profile/delete-account \
-H "Content-Type: application/json" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
-d '{
"password": "mypassword123"
}'Request (social login):
curl -X POST http://localhost:3100/api/profile/delete-account \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."Request Body:
| Field | Type | Required | Description | |-------|------|----------|-------------| | password | string | Manual login only | Current password for confirmation |
Response (200):
{
"status": 1,
"message": "Account deleted",
"data": null
}What happens: Account status set to
deleted_by_user, all OTPs deleted, all refresh tokens deleted, cache cleared. The user can register again with the same email.
Customization Guide
Generators Run Once
When you run add auth or add crud products, the generator creates files in your project. After that, the code is yours. The generator will not overwrite or update these files.
This means:
- You can freely edit any generated file
- Adding fields to a model? Edit
src/models/{module}.tsdirectly - Changing controller logic? Edit
src/controllers/common/{module}/controller.ts - The generator is a starting point, not a framework that controls your code
Can I Re-Run a Generator?
add auth— checks if auth is already installed. If yes, it skips (won't overwrite).add crud products— checks if the model already exists. If yes, it skips (won't overwrite).
To regenerate, you'd need to delete the existing files first. But generally, just edit the files directly.
What's Safe to Edit
| File | Safe to Edit? | Notes |
|------|--------------|-------|
| src/models/*.ts | Yes | Add fields, change validations, add indexes |
| src/controllers/**/*.ts | Yes | Change business logic, add custom endpoints |
| src/routes/**/*.ts | Yes | Add routes, change middleware |
| src/config/roles.seed.ts | Yes | Add roles, change permissions (re-seed required) |
| src/utility/local/config/auth_config.ts | Yes | Change token expiry, password rules, enable social login |
| src/middlewares/*.ts | Yes | Customize auth checks |
| src/utility/services/*.ts | Yes | Customize email templates, cron schedules |
Configuration
Auth Config (src/utility/local/config/auth_config.ts)
Restart your server after changing any config. Config is loaded on startup.
| Setting | Default | Description |
|---------|---------|-------------|
| manual_login.enabled | true | Enable email/password login |
| social_login.google.enabled | false | Enable Google OAuth. Also requires GOOGLE_CLIENT_ID_* env vars |
| social_login.apple.enabled | false | Enable Apple Sign In. No env vars needed |
| token.expiry | "24h" | Default access token expiry |
| token.remember_me_expiry | "30d" | Expiry when "remember me" is used |
| token.role_expiry.super_admin | "1h" | Access token expiry for super admins |
| token.role_expiry.admin | "4h" | Access token expiry for admins |
| token.role_expiry.user | "24h" | Access token expiry for users |
| token.refresh.expiry_days.super_admin | 3 | Refresh token lifetime (days) for super admins |
| token.refresh.expiry_days.admin | 7 | Refresh token lifetime (days) for admins |
| token.refresh.expiry_days.user | 30 | Refresh token lifetime (days) for users |
| token.refresh.max_sessions.super_admin | 1 | Max concurrent sessions for super admins |
| token.refresh.max_sessions.admin | 3 | Max concurrent sessions for admins |
| token.refresh.max_sessions.user | 5 | Max concurrent sessions for users |
| otp.expiry_minutes | 10 | OTP expiration time in minutes |
| otp.length | 6 | Number of digits in OTP |
| password.min_length | 6 | Minimum password length |
| password.require_uppercase | false | Require
