Auth module
The auth template is an explicit authentication starter module for ExetJS. It provides register, login, logout and current-user routes, password hashing, JWT helpers and a replaceable repository contract.
Full public documentation is available at https://exetjs.com/.
Install
exet install auth
The Auth template shipped with ExetJS CLI 0.2.0 requires @exetjs/core 0.2.0 or newer.
The installer adds:
JWT_SECRET=
JWT_EXPIRES_IN=1d
and installs:
bcryptjs
jsonwebtoken
@types/jsonwebtoken
Default route prefix
The module uses:
name: "auth";
basePath: "/auth";
With the default application API prefix /api/v1, the routes are:
| Method | Route | Purpose |
|---|---|---|
POST | /api/v1/auth/register | Register a user |
POST | /api/v1/auth/login | Authenticate and issue a JWT |
POST | /api/v1/auth/logout | Return a successful logout response |
GET | /api/v1/auth/me | Resolve the current user from a bearer token |
If the application changes API_PREFIX, the route prefix changes with it.
Request validation
Register
{
"name": "Jane Doe",
"email": "jane@example.com",
"password": "at-least-8-characters"
}
Validation rules:
| Field | Rule |
|---|---|
name | string, minimum 2 characters |
email | valid email address |
password | string, minimum 8 characters |
A successful registration returns HTTP 201 and does not expose the password hash.
Example response:
{
"success": true,
"data": {
"id": "generated-user-id",
"name": "Jane Doe",
"email": "jane@example.com",
"createdAt": "2026-09-20T00:00:00.000Z"
}
}
Login
{
"email": "jane@example.com",
"password": "at-least-8-characters"
}
Validation rules:
| Field | Rule |
|---|---|
email | valid email address |
password | non-empty string |
A successful login returns HTTP 200:
{
"success": true,
"data": {
"user": {
"id": "generated-user-id",
"name": "Jane Doe",
"email": "jane@example.com",
"createdAt": "2026-09-20T00:00:00.000Z"
},
"token": "jwt-token"
}
}
Invalid credentials return an unauthorized application error without identifying whether the email or password was wrong.
Current user
Send the JWT as a bearer token:
Authorization: Bearer <token>
GET /api/v1/auth/me
Missing, invalid or expired bearer tokens are rejected. A token is also rejected if its user can no longer be found by the configured repository.
Logout
POST /api/v1/auth/logout
The starter currently returns:
{
"success": true,
"data": {
"loggedOut": true
}
}
The starter uses stateless JWTs and does not maintain a revocation list or server-side session. Calling logout therefore does not invalidate an already issued token. Applications that require immediate token revocation must add their own session, token blacklist, rotation or revocation policy.
Environment variables
| Variable | Required | Default | Notes |
|---|---|---|---|
JWT_SECRET | production: yes | development fallback only | Use a strong private value in production |
JWT_EXPIRES_IN | no | 1d | Passed to jsonwebtoken as the token lifetime |
In NODE_ENV=production, startup or first JWT use without JWT_SECRET throws an error. The built-in development fallback exists only for local development and must not be relied on in production.
Password hashing
Passwords are hashed with bcryptjs using 12 salt rounds. Password hashes are stored only through the repository contract and are never returned by the public Auth API.
Repository contract
The public storage contract is:
export type AuthRepository = {
create: (user: AuthUser) => Promise<AuthUser>;
findByEmail: (email: string) => Promise<AuthUser | null>;
findById: (id: string) => Promise<AuthUser | null>;
};
The default repository is in-memory:
createInMemoryAuthRepository();
createAuthRepository is retained as a compatibility alias for the same in-memory implementation.
The in-memory repository is scoped to one Auth module factory instance and is not persistent. Restarting the process loses all users.
Module factory
import { createAuthModule } from "./modules/auth/index.js";
const authModule = createAuthModule({
repository,
name: "auth",
dependsOn: ["db-postgres"],
});
Options:
| Option | Type | Default | Purpose |
|---|---|---|---|
repository | AuthRepository or factory | in-memory repository | Replace storage implementation |
name | string | auth | Required to distinguish multiple Auth instances |
dependsOn | readonly string[] | none | Declare DB/resource dependencies used by the repository |
A repository factory receives the ExetContext and may be asynchronous:
const authModule = createAuthModule({
dependsOn: ["db-postgres"],
repository: async (ctx) => {
ctx.logger.info("Creating persistent Auth repository");
return createPostgresAuthRepository();
},
});
Connecting Auth to a database
DB modules own connection lifecycle. Auth owns authentication behavior. The application repository owns schema mapping and persistence.
Example shape with PostgreSQL:
import type { AuthRepository } from "./auth/index.js";
import { createAuthModule } from "./auth/index.js";
import { dbPostgres } from "./db-postgres/index.js";
const authRepository: AuthRepository = {
async create(user) {
await dbPostgres.getClient().query(
`INSERT INTO auth_users (id, name, email, password_hash, created_at)
VALUES ($1, $2, $3, $4, $5)`,
[user.id, user.name, user.email, user.passwordHash, user.createdAt],
);
return user;
},
async findByEmail(email) {
const result = await dbPostgres.getClient().query(
`SELECT id, name, email, password_hash AS "passwordHash", created_at AS "createdAt"
FROM auth_users WHERE email = $1`,
[email],
);
return result.rows[0] ?? null;
},
async findById(id) {
const result = await dbPostgres.getClient().query(
`SELECT id, name, email, password_hash AS "passwordHash", created_at AS "createdAt"
FROM auth_users WHERE id = $1`,
[id],
);
return result.rows[0] ?? null;
},
};
export const authModule = createAuthModule({
repository: authRepository,
dependsOn: ["db-postgres"],
});
Create the schema, unique email constraint and migrations in application-owned migration files. The Auth template does not create tables, indexes or migrations.
Public exports
The installed module exports:
authModule;
createAuthModule;
createAuthRepository;
createInMemoryAuthRepository;
and these public types:
AuthModuleOptions;
AuthRepositoryFactory;
AuthRepository;
AuthUser;
PublicAuthUser;
AuthTokenPayload;
Production checklist
Before production use:
- replace the in-memory repository with persistent storage;
- create and version the required schema and migrations;
- enforce a unique normalized email policy appropriate for the application;
- set a strong
JWT_SECREToutside version control; - choose an appropriate
JWT_EXPIRES_INvalue; - add rate limiting and abuse protection where required;
- add account verification, password reset and credential recovery when required;
- add session or token revocation behavior if logout must invalidate tokens immediately;
- define refresh-token and rotation behavior if long-lived sessions are required;
- review audit logging and privacy requirements for authentication events.
The starter is intentionally small and explicit. It is not a complete identity platform.