Skip to content

Jadu Auth (Backend)

Central authentication server for Scenarix applications. Handles user registration, login, session management, email verification (OTP), SMS OTP login, password reset, and RBAC (apps, roles, permissions).

Tech Stack

  • Runtime: Node.js with TypeScript
  • Framework: Express.js v5
  • Database: MongoDB (native driver)
  • Auth: Better Auth with JWT plugin (EdDSA), session (bearer) and admin plugins
  • Validation: Zod
  • API docs: OpenAPI 3.0 + Swagger UI

Quick Start

  1. Clone and install
cd jadu-auth-be
npm install
  1. Environment

Copy .env.example to .env and set at least:

  • PORT – server port (default 8084)
  • MONGODB_URI_JADU_AUTH – MongoDB connection string
  • BETTER_AUTH_SECRET – secret key (32+ characters); never commit
  • BETTER_AUTH_URL – base URL of this server (e.g. http://localhost:8084)
  • TRUSTED_ORIGINS – comma-separated allowed origins (e.g. frontend URL)

See .env.example for mail (SES/SMTP), SMS OTP (Twilio), email verification, and password reset options.

  1. Run
npm run dev

Server: http://localhost:8084
API docs: http://localhost:8084/api-docs

Main Endpoints

Area Examples
Auth POST /api/auth/signup, POST /api/auth/login, POST /api/auth/logout, POST /api/auth/refresh, GET /api/auth/me
Password POST /api/auth/forgot-password, POST /api/auth/reset-password, POST /api/auth/change-password
Email verification POST /api/auth/send-verification-email, POST /api/auth/verify-email
Sessions GET /api/auth/sessions, DELETE /api/auth/sessions/:sessionToken
JWKS GET /.well-known/jwks (for JWT verification by other services)
Admin GET/PUT /api/admin/users, GET /api/admin/users/:userId (admin role required)
RBAC GET/POST/PUT/DELETE /api/admin/apps, roles, permissions, user role assignment (admin required)
Discord POST /api/discord/interactions (Discord bot slash command + modal verification — see below)

Full list and request/response shapes: Swagger UI at /api-docs or OpenAPI JSON at /api-docs.json.

Documentation

Backend docs live in docs/:

Code style and naming: CONVENTIONS.md.

Project Layout

  • src/index.ts – Express app, CORS, Better Auth mount, routes, Swagger
  • src/lib/ – auth (Better Auth config), db, jwt helpers, OpenAPI base, verification helper
  • src/auth/ – auth router, controller, service, validators, middleware, providers (email/password, session, email verification)
  • src/admin/ – admin router, controller, service, middleware, validators
  • src/rbac/ – RBAC router, controller, service, types, schemas, validators, OpenAPI
  • src/mail/ – mail service, provider (SMTP/SES), template renderer, types
  • src/models/ – users, apps, OTP (MongoDB collections)
  • src/shared/ – constants, errors, helpers (response, error handler, UUID)

See docs/ARCHITECTURE.md for a detailed folder map and CONVENTIONS.md for naming and request flow.

Scripts

Command Description
npm run dev Start with nodemon (ts-node)
npm run build Compile TypeScript to dist/
npm start Run dist/index.js
npm run lint ESLint
npm run setup Install Lefthook git hooks (pre-push only when be/ is pushed: lint, build, integration tests with 98% coverage). Run from repo root with npm run setup or from be/; config lives at repo root lefthook.yml.
npm run discord:register-commands One-time per guild: registers the /verify slash command (needs DISCORD_CLIENT_ID + DISCORD_BOT_TOKEN + DISCORD_GUILD_ID).
npm run discord:post-welcome Posts (or edits, if DISCORD_START_HERE_MESSAGE_ID is set) the welcome message + "Verify" button into #start-here.

Discord "Verified Creator" role (in-Discord bot flow)

Members of the Studio Jadu Discord get a role automatically once they link their Jadu account — entirely inside Discord, no browser, no Discord OAuth. Entry points: the /verify slash command, or the "Verify your account" button in #start-here (posted by discord:post-welcome) — both open the same email modal.

/verify or #start-here button
  → email modal (Discord-native popup)
  → submit → defer, then real Jadu OTP sent (EmailOtpService.loginInit, so the same
             per-email 60s resend cooldown as the web login applies) → ephemeral message + "Enter code" button
             (a button, not a second modal directly — Discord doesn't support opening a modal
             as the reply to a modal submission, so a button click carries the state instead)
  → click button → code modal
  → submit → defer, then check the OTP (OtpService, no session or tokens are created —
             this proves email ownership, it is not a login) → link accounts
             (discordLinks.model.ts) → RBAC auto-grant → bot assigns the role directly
             (PUT /guilds/{g}/members/{u}/roles/{r}) → ephemeral "✅ Verified!" message

The Discord identity comes straight from the interaction's signature (member.user), never from OAuth — Discord cryptographically signs every interaction with the real invoking user's ID, so there's nothing to spoof and no refresh token to store.

Implementation: src/discord/bot/; Discord HTTP client: src/thirdPartyServices/discord/; auth app config (discord_verify, auto-grants USER on first use): src/authApps/discordVerify.config.ts. Env vars: see the Discord block in .env.example.

Why a bot instead of Discord's Linked Roles feature? Linked Roles requires the user to manually claim the role afterward (Discord's own docs: "this ensures members actually want the role, rather than auto-assigning them" — no API/bot workaround exists) and is poorly discovered (buried in a server-name dropdown menu). A bot with Manage Roles assigns an ordinary role directly — zero claim step — which is how virtually every commercial "verified member" integration works (Patreon, Collab.Land, Ko-fi, Whop, Guild.xyz).

Per-environment Discord app setup (Developer Portal): OAuth2 scopes aren't needed at all for this flow. Copy the Application ID (DISCORD_CLIENT_ID) and Bot Token (DISCORD_BOT_TOKEN) from the app, and the Public Key (DISCORD_PUBLIC_KEY) from General Information. Invite the bot with OAuth2 → URL Generator → scope bot + permission Manage Roles (this is what makes it an actual guild member — enabling only Linked Roles/slash commands does not). In the server: create a plain (non-Linked) role for the bot to assign, position the bot's own role above it in Server Settings → Roles (Discord's permission hierarchy rule — the grant call 403s otherwise), then run discord:register-commands and discord:post-welcome once each.

Security Notes

  • Use a strong BETTER_AUTH_SECRET (32+ chars) and keep it out of version control.
  • In production, set TRUSTED_ORIGINS to your real frontend/backend URLs.
  • Session cookie is HttpOnly, and secure in production; access token is short-lived (15 min).
  • Forgot-password and send-verification-email responses do not reveal whether an email exists.
  • SMS OTP login-init responses do not reveal whether a phone number exists.
  • Exception, by design: the Discord bot flow tells the user when no Jadu account matches the email they typed. Without it a typo looks identical to a delayed email and the user is stuck. The reply is ephemeral (visible only to them), and it reveals nothing the public email-OTP login-init endpoint does not already reveal with its "No account found" response.

A more detailed code and security review is in REVIEW.md.

License

ISC