Authentication
Sep 4, 2025

Passwordless authentication setup with Express.js and Scalekit

TL;DR:

  • Lazy SDK init: Scalekit client loads only on first auth call, preventing boot failures if env vars or network are misconfigured.
  • Minimal route surface: /send | /resend | /verify/code | /verify | /me | /logout handle the full passwordless cycle, replacing brittle reset flows with predictable APIs.
  • Middleware chain: express.json, Helmet, CORS, sessions, rate limits, and logging form a resilient request pipeline that blocks abuse and improves observability.
  • Session durability: Express sessions backed by Redis ensure logins persist across tabs and replicas, fixing the “revolving door” login problem.
  • Production hardening: Centralized error handler, strict CORS, HTTPS-only cookies, and Dockerized deployment turn the passwordless flow into a portable, compliant, and resilient system.

Conference Wi-Fi and the hidden cost of passwords

On the first day of a large developer conference like KubeCon or SaaStr, hundreds of attendees attempt to connect to the venue Wi-Fi. The portal requires a password that was last set a year ago. Reset links arrive slowly, the login page struggles on mobile, and within minutes, frustrated developers abandon the system in favor of tethering their phones. The damage is more than inconvenience; organizers lose goodwill, sponsors lose visibility, and the infrastructure team loses trust. The real failure wasn’t the wireless hardware, but the outdated authentication model that created unnecessary friction at scale.

For engineering leaders, this story is familiar across industries. Passwords create a drag on user experience, inflate operational overhead, and increase exposure to security risks. Every reset email is a support cost. Every reused credential is a liability. Every failed login is lost engagement. By contrast, passwordless authentication replaces this complexity with a direct, one-step action: a magic link or a short code delivered to the user’s inbox. No credentials to remember, no reset workflows to maintain, and no attack surface for brute-force attempts.

This guide demonstrates how to implement a production-grade passwordless backend using Express.js and Scalekit. It covers SDK initialization, route handlers, middleware design, session storage, CORS policies, error handling, security best practices, and Docker deployment, a complete blueprint for running passwordless authentication in production.

Why passwordless solves the real problem

The conference Wi-Fi fiasco wasn’t about bandwidth. It was about authentication friction. Attendees didn’t struggle because of slow internet; they struggled because a password-based system forced them to remember something irrelevant from last year. That same friction plays out in SaaS apps, internal dashboards, and consumer platforms every day.

Passwordless authentication removes that barrier. Instead of credentials users forget, reuse, or reset under stress, the system issues a one-time action, either a magic link or a short verification code sent directly to the inbox. The experience shifts from “prove what you know” to “confirm who you are,” cutting failure points dramatically. For organizations, that means:

  • Fewer support tickets: No more “reset my password” loops.
  • Reduced risk exposure: No stored credentials to leak or brute-force.
  • Faster onboarding: One-click entry for first-time users.
  • Better engagement: Users actually log in instead of giving up.

In the conference example, passwordless would have made the login process invisible: attendees tap the link on their phone and they’re online. No resets, no queues, no support escalation.

Installing the Scalekit SDK and dependencies

The flow we’re aiming to implement is the same one that failed at the conference: a user enters their email, receives either a one-time code or a magic link, and verifies to establish a session. The difference is that instead of juggling reset forms and brittle credential storage, we’ll power this flow with Scalekit’s passwordless APIs inside an Express.js backend.

Scalekit provides the primitives for sending, resending, and verifying passwordless emails. Express gives us the familiar Node.js server framework to orchestrate routes, sessions, and middleware around those APIs. Together, they form a minimal but production-lean project template: a backend that can be read end-to-end in minutes and deployed without guesswork. You can get the full sample project in this GitHub repo or bulid it from scratch following the guide.

To start building, install the Scalekit SDK along with the supporting Express and middleware dependencies:

npm install @scalekit-sdk/node express express-session helmet compression cors morgan joi express-rate-limit

This set of dependencies covers:

  • @scalekit-sdk/node → Scalekit passwordless authentication SDK
  • express → Web framework for route handling
  • express-session → Session storage for logged-in users
  • helmet → Security headers
  • compression → Gzip responses for performance
  • cors → Cross-origin resource sharing rules
  • morgan → Structured logging
  • joi → Environment variable and input validation
  • express-rate-limit → Protection against abuse

Together, these libraries cover the essentials: HTTP hardening, traffic control, observability, and session integrity, all critical in high-concurrency login flows.

At this point, your project is equipped with everything needed to replicate the conference Wi-Fi scenario, only this time with passwordless authentication that won’t collapse under user load.

Initializing the Scalekit SDK for resilient authentication

At the conference, the Wi-Fi portal didn’t just frustrate users; it also created chaos behind the scenes. The IT team couldn’t diagnose failures quickly because the authentication layer was scattered across multiple services. When you adopt passwordless with Scalekit, initialization matters just as much as the flow itself. If the SDK is set up poorly, a single misstep, like a missing environment variable, can prevent your entire service from starting.

To avoid that fragility, the Scalekit Node SDK supports lazy initialization. Instead of connecting to Scalekit at server startup, the client is created only when an authentication call is actually made. This ensures that a temporary outage or misconfiguration doesn’t block deployments or bring down unrelated endpoints. For a high-stakes environment like the conference, it means the Wi-Fi portal would stay online even if Scalekit were briefly unreachable.

After installing the dependencies, the next step is to configure how your app connects to Scalekit. This isn’t a command to run; it’s a small module you add to your project, allowing other parts of your Express app to safely import a single, initialized client. 

Create a new file at config/scalekit.js and add the following code:

npm install @scalekit-sdk/node express express-session helmet compression cors morgan joi express-rate-limit

To prevent silent failures during deployment, environment variables like SCALEKIT_CLIENT_ID and SESSION_SECRET should be validated at startup (e.g., with Joi).

Startup and lazy initialization: env validation and first auth call

Boot sequence: Joi validates env, Express starts, Scalekit client lazily initializes on first auth call.

This guarantees:

  • Single client instance: No duplicate connections across routes.
  • Deferred network cost: No delays during server boot.
  • Safer deployments: An invalid credential won’t crash unrelated services.

With the SDK initialized this way, every route in your Express app can import getScalekit() and stay focused on business logic instead of connection management.

Defining route handlers for a clean passwordless flow

Once the Scalekit client is initialized, the next step is exposing routes in Express that use it. These routes are where the passwordless flow actually happens: sending a login email, verifying a code or magic link, establishing a session, and checking who the current user is.

This is exactly where the conference Wi-Fi portal went wrong. It forced attendees through brittle, multi-step resets and glitchy forms, adding latency and more ways to fail. By contrast, Scalekit condenses the entire cycle into a few predictable endpoints that stay consistent under load.

In the Express backend, these routes cover the complete passwordless cycle:

<INSERT>

Example: sending a passwordless email

We’ll now implement the first two endpoints from the table using the initialized Scalekit client. Create src/routes/auth.js and wire these handlers. (We’ll mount this router in app.js in a moment.)

// routes/auth.js const express = require('express'); const getScalekit = require('../config/scalekit'); const router = express.Router(); router.post('/passwordless/email/send', async (req, res, next) => { const { email } = req.body; try { const client = getScalekit(); const response = await client.passwordless.sendPasswordlessEmail(email, { template: 'SIGNIN', magiclinkAuthUri: `${process.env.NEXT_PUBLIC_BASE_URL}/api/auth/passwordless/verify` }); res.json(response); } catch (err) { next(err); } }); module.exports = router;

What this does:

Validates the email and calls sendPasswordlessEmail.

  • Returns { authRequestId, expiresIn, passwordlessType } so the client can either:
  • Prompt for an OTP and pass authRequestId to /passwordless/email/verify/code, or
  • Wait for the user to click the magic link, which will hit /passwordless/verify.

Note on magiclinkAuthUri:

  • This is the redirect URI users land on after clicking the email link.
  • It should point to your backend handler that completes the magic-link flow (we’ll add it below).

Example: verifying a one-time passcode (OTP)

Next, the OTP path. This route verifies a user-entered code (plus the authRequestId from the send step), then establishes a session.

// routes/auth.js router.post('/passwordless/email/verify/code', async (req, res, next) => { const { code, authRequestId } = req.body; try { const client = getScalekit(); const response = await client.passwordless.verifyPasswordlessEmail({ code }, authRequestId); req.session.user = { email: response.email }; res.json({ user: req.session.user }); } catch (err) { next(err); } });

By consolidating the flow into these six endpoints, teams eliminate the sprawling “forgot password” jungle. For attendees in our Wi-Fi story, this would have meant one route: send, followed by a one-tap magic link. No reset queues, no mobile form bugs, no abandoned logins.

After verification, the session is set on req.session.user. Expose /api/me so clients can confirm login state:

// routes/health.js (or a small /me route file) const express = require('express'); const router = express.Router(); router.get('/me', (req, res) => { if (!req.session?.user) return res.status(401).json({ error: 'Unauthorized' }); res.json({ user: req.session.user }); }); module.exports = router;

OTP verification path: request, provider verification, session creation, and consistent JSON errors:

OTP verification express route behavior

Magic link verification with CORS and optional same-origin authRequestId enforcement:

Magic link verification CORS with same origin enforcement

Before we wire any more endpoints, we need the foundation that makes them safe and observable in production. That foundation is the middleware chain: body parsing, security headers, CORS, sessions, rate limiting, and logging, in that order. Without it, the same flows will degrade under load just like the conference portal.

Applying middleware patterns for security and resilience

The conference Wi-Fi login didn’t just fail at the surface; it failed deeper in the stack. Each request bounced through uncoordinated checks: ad-hoc CORS rules, incomplete logging, and no consistent rate limits. When errors piled up, operators had little visibility into what was happening. Middleware is where these concerns should have been handled from the start.

Why these layers matter (mapped to real risks):

  • Body parser (express.json) → Prevents malformed bodies and enforces safe size limits.
  • Helmet → Mitigates common web attacks via security headers (XSS, clickjacking).
  • Compression → Keeps responses fast under load (especially on mobile networks at events).
  • CORS → Ensures magic-link requests from your allowed domains aren’t blocked by the browser.
  • Session middleware → Persists auth state; swaps to Redis for multi-replica stability.
  • Rate limiting → Caps send/resend attempts to protect email channels and reduce abuse.
  • Logging (morgan) → Creates a forensic trail for incident response and performance tuning.

Example wiring in app.js:

Open src/app.js and register middleware in this order (top-to-bottom), before mounting your routes:

const express = require('express'); const helmet = require('helmet'); const compression = require('compression'); const morgan = require('morgan'); const cors = require('cors'); const session = require('./middleware/session'); const rateLimits = require('./middleware/rateLimits'); const app = express(); app.use(helmet()); app.use(compression()); app.use(morgan(process.env.LOG_FORMAT || 'dev')); app.use(cors({ origin: process.env.CORS_ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000'], credentials: true })); app.use(session); app.use(rateLimits.global);

This layered approach means the API degrades gracefully even under stress. At the conference, instead of users hammering the reset form until the provider throttled them, a rate limiter would have responded with clear limits and preserved system stability. Operators would have had logs to analyze, and attendees would have still been able to log in with a single magic link.

Abuse protection: Send and resend rate limits

Abuse controls on email send/resend: global and per-email throttles, with clear 429 responses.

With this pipeline in place, the same passwordless routes survive real-world traffic: requests are parsed, origins are verified, sessions stick across replicas, abusers are throttled, and operators have logs to act on, exactly what the conference portal lacked.

Storing sessions reliably with in-memory or Redis

At the conference, even when a few attendees managed to reset their passwords, sessions didn’t always persist. Opening the portal in another tab forced them to log in again. That inconsistency wasn’t about cookies; it was about how sessions were stored. Without a reliable session layer, authentication becomes a revolving door.

In an Express + Scalekit setup, sessions are central. Once a user verifies their identity through OTP or magic link, the backend needs to maintain their authenticated state. By default, Express uses an in-memory session store, which is fine for demos but breaks down under load or across multiple servers. In production, a distributed store like Redis ensures consistency across instances.

Example session middleware:

// middleware/session.js const session = require('express-session'); const RedisStore = require('connect-redis').default; const Redis = require('ioredis'); const redisClient = process.env.REDIS_URL ? new Redis(process.env.REDIS_URL) : null; module.exports = session({ store: redisClient ? new RedisStore({ client: redisClient }) : undefined, secret: process.env.SESSION_SECRET || 'dev-secret', resave: false, saveUninitialized: false, cookie: { httpOnly: true, sameSite: 'lax', secure: process.env.NODE_ENV === 'production' } });
Session consistency: Load balanced replicas and redis

Session creation on verify and subsequent retrieval via /api/me across load-balanced replicas with Redis. This approach provides:

  • In-memory simplicity → Fast setup for local testing.
  • Redis-backed scaling → Shared sessions across replicas.
  • Security by default → HTTP-only, same-site cookies prevent tampering.

If the Wi-Fi system had relied on a Redis-backed session layer, attendees wouldn’t have been logged out mid-session or forced through repeated resets. For enterprise systems, that stability translates directly to user trust and reduced support calls.

Configuring CORS for secure magic link redirects

One overlooked issue at the conference was device switching. Some attendees requested a reset link on their phone, but the link opened a login page hosted on a different subdomain than the Wi-Fi portal. The browser rejected the request due to cross-origin rules, leaving users stranded even after clicking the right link. This is exactly where proper CORS configuration matters.

In passwordless flows, magic links redirect back to your backend. If CORS isn’t configured correctly, the browser will block those requests, breaking verification. The solution is to explicitly allow the origins your frontend or portal will use.

Example configuration:

const cors = require('cors'); app.use(cors({ origin: process.env.CORS_ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000'], credentials: true, }));

Key points:

  • Whitelist specific origins,  never allow * in production.
  • Enable credentials so session cookies flow correctly.
  • Use environment variables to keep configuration consistent across staging and production.

In the conference scenario, this would have ensured that links opened from mobile email apps worked seamlessly, regardless of whether the user landed on wifi.conf.com or auth.conf.com. For SaaS platforms, the same principle applies: consistent, secure CORS rules make passwordless login work reliably across browsers and devices.

Centralizing error handling for clarity and trust

At the conference, one of the most frustrating parts wasn’t just being locked out; it was the silence. Some attendees saw a spinning loader that never resolved. Others saw a vague “something went wrong.” Without clear error messages, they didn’t know whether to retry, reset again, or give up. This lack of transparency eroded trust in the system just as much as the broken login did.

In an Express + Scalekit backend, a centralized error handler ensures that every failure returns a structured, predictable response. Instead of leaking raw SDK exceptions or leaving users hanging, the API responds with JSON that clients can interpret cleanly. Developers get actionable logs, users get clear feedback, and operators don’t have to chase phantom bugs.

Example error handler:

// utils/errorHandler.js function errorHandler(err, req, res, next) { console.error(err); // could be replaced by structured logging res.status(err.status || 500).json({ error: err.message, details: err.details || undefined, }); } module.exports = errorHandler;
// app.js const errorHandler = require('./utils/errorHandler'); app.use(errorHandler);

Benefits of this pattern:

  • Consistency → Every error follows the same format.
  • Observability → Logs capture root causes without overwhelming operators.
  • User trust → Clear messages like “Invalid or expired code” instead of “Internal Server Error.”

Applied to the conference Wi-Fi story, this would have turned vague loaders into precise guidance: “Your link expired, request a new one.” Instead of hundreds of attendees tethering their phones, most would have retried successfully. In enterprise systems, the same principle preserves confidence at scale.

Following security best practices from the start

The conference Wi-Fi login wasn’t only frustrating; it was risky. Password resets, reused credentials, and inconsistent sessions created an attractive attack surface. A malicious actor could have intercepted weak reset links, brute-forced short-lived passwords, or hijacked unencrypted sessions. What began as a UX failure could easily have escalated into a security incident.

In Express + Scalekit, passwordless authentication already reduces exposure by eliminating stored credentials. But security still depends on the surrounding practices:

  • Session secrets → Always set strong SESSION_SECRET and JWT_SECRET values; never rely on auto-generated ones in production.
  • Redis-backed sessions → Use Redis for distributed, tamper-resistant session management.
  • HTTPS enforcement → Mark cookies as secure and run behind TLS.
  • CORS restrictions → Limit origins to known frontends only.
  • Helmet policies → Add Content Security Policy (CSP) and disable unnecessary headers.
  • CSRF protection → Enable tokens if browser-based forms are supported.
  • Rate limiting → Apply per-user and per-IP throttles to send endpoints.
  • Audit and logging → Capture authentication attempts for anomaly detection.

By layering these controls, you shift authentication from a fragile gate into a hardened access point. In the conference case, these steps would have prevented replay attacks and limited brute-force attempts even under heavy traffic. For enterprises, they mean regulatory alignment (e.g., SOC 2, PCI-DSS) and fewer late-night incident calls.

Packaging and deploying with Docker for consistency

At the conference, one reason the Wi-Fi portal spiraled into chaos was the lack of environmental parity. The authentication service behaved differently across staging, production, and mobile subdomains. When issues appeared, fixes that worked in one environment failed in another. This inconsistency is exactly what containerization solves.

With Docker, the Express + Scalekit backend runs the same way on a laptop, staging cluster, or production servers. All dependencies, configs, and runtime versions are packaged together, eliminating “it works on my machine” surprises during critical events like product launches or high-traffic conferences.

Minimal Dockerfile for production:

FROM node:20-alpine WORKDIR /app COPY package*.json ./ RUN npm ci --only=production COPY . . CMD ["npm", "start"]

For distributed sessions and local testing, Docker Compose adds Redis automatically:

services: app: build: . ports: ["3000:3000"] env_file: .env redis: image: redis:7

Run locally with:

docker compose up --build

This guarantees that authentication logic, session handling, and Scalekit integration behave consistently regardless of where the service is deployed. In the Wi-Fi portal’s case, containerized deployment would have prevented environment drift and ensured the same tested image powered both test networks and the live conference. For SaaS teams, it means smoother rollouts, faster recovery, and fewer late-night firefights.

Conclusion: From conference chaos to seamless authentication

The conference Wi-Fi fiasco showed how much damage brittle authentication can cause. Forgotten passwords, broken reset flows, and inconsistent sessions turned a simple login into a failed user experience. The cost wasn’t just inconvenience; it was lost trust, abandoned usage, and wasted operational cycles.

By replacing passwords with Scalekit-powered passwordless authentication in an Express.js backend, the scenario unfolds differently. Attendees receive a one-tap magic link or a short code; sessions persist reliably across devices; errors return clear JSON instead of cryptic loaders; and operators retain visibility through centralized logging and rate limits. What was once a liability becomes a frictionless access layer that scales smoothly under pressure.

Through this guide, you’ve seen how to:

  • Initialize the Scalekit SDK safely with lazy loading
  • Define clean route handlers for OTP and magic link flows
  • Apply middleware patterns for security, logging, and performance
  • Store sessions reliably with Redis in production
  • Configure CORS for cross-device magic link verification
  • Centralize error handling for clarity and trust
  • Implement security best practices to harden the system
  • Package everything in Docker for portable, predictable deployment

For experienced developers, this provides a template backend that is small enough to read in minutes yet extensible enough for production. For leaders, it demonstrates how passwordless authentication translates directly into lower operational costs, a stronger security posture, and higher user engagement. Whether it’s a SaaS launch, an e-commerce flash sale, or your next conference, Express.js + Scalekit ensures authentication is the part of the system you never have to worry about again.

FAQ

How does Scalekit handle OTP and magic link verification in Express.js?

Scalekit provides SDK methods like sendPasswordlessEmail, resendPasswordlessEmail, and verifyPasswordlessEmail. In Express.js, these map directly to route handlers, allowing you to verify OTP codes or magic link tokens and establish sessions without implementing complex authentication logic manually.

Can Scalekit passwordless authentication be scaled across multiple servers?

Yes. Scalekit works seamlessly in distributed environments when paired with a shared session store like Redis. This ensures verified sessions remain consistent across replicas, avoiding the pitfalls of in-memory storage.

Should I use JWT or sessions for passwordless authentication in Express.js?

Both work, but sessions are generally simpler for web applications since they use HTTP-only cookies. JWTs are useful for APIs or microservices where stateless authentication is needed. A hybrid approach, sessions for browsers, JWT for APIs, is common in production systems.

How do I secure Express.js middleware for production passwordless login?

Key practices include using Helmet for HTTP headers, enabling TLS for secure cookies, applying per-user rate limits to login endpoints, restricting CORS origins, and centralizing error handling. These reduce risks like CSRF, brute-force attacks, and cross-origin abuse.

What are the performance considerations for passwordless authentication at scale?

Performance bottlenecks often come from email/SMS providers, not the authentication logic itself. Use asynchronous queues for sending OTP or magic link emails, cache session lookups in Redis, and implement request-level rate limiting to prevent abuse. Express.js + Scalekit handles high concurrency well when paired with these practices.

No items found.
On this page
Share this article
Modernize login flows

Acquire enterprise customers with zero upfront cost

Every feature unlocked. No hidden fees.
Start Free
$0
/ month
1 FREE SSO/SCIM connection each
1000 Monthly active users
25 Monthly active organizations
Passwordless auth
API auth: 1000 M2M tokens
MCP auth: 1000 M2M tokens