npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@onderwijsin/directus-magic-links-bundle

v0.4.1

Published

Passwordless magic-link authentication for Directus frontend clients

Readme

@onderwijsin/directus-magic-links-bundle

Passwordless magic-link authentication for Directus frontend clients.

Optional scheduled cleanup removes old expired and redeemed records.

⚠️ Magic links are only support for users with native auth provider. OAuth providers are not supported.

Installation

Install the bundle into a Directus project:

pnpm add @onderwijsin/directus-magic-links-bundle

The bundle requires a configured Directus email transport and at least one redirect URL in MAGIC_LINKS_REDIRECT_URL_ALLOWLIST. Redirect URLs may include explicit ports, such as http://localhost:3000/auth/magic-link; use HTTPS in production.

Configuration

The endpoint and startup hook each validate the shared environment configuration. While some of the configuration options are specific to this bundle, it also relies on common directus configuration.

| Variable | Default | Description | | -------------------------------------------------------------- | -------------------------------------------------- | --------------------------------------------------------------------------------- | | MAGIC_LINKS_ENABLED | true | Enable the bundle entries. | | DIRECTUS_EXTENSIONS_SCHEMA_CHANGES_ENABLED | true | Global schema-change switch. | | MAGIC_LINKS_SCHEMA_CHANGES_ENABLED | true | Enable this bundle's schema changes. | | MAGIC_LINKS_SCHEMA_ABORT_ON_ERROR | true | Abort bundle setup after an unexpected schema error. | | SYNCHRONIZATION_STORE | memory | Global fallback for the lock and limiter stores. | | DIRECTUS_EXTENSIONS_LOCK_PROVIDER | unset | Schema lock provider: memory, redis, or fs; otherwise uses synchronization. | | DIRECTUS_EXTENSIONS_LOCK_REDIS_URL | unset | Optional override; otherwise uses resolved Redis settings. | | DIRECTUS_EXTENSIONS_LOCK_FS_DIRECTORY | unset | Required when the provider is fs. | | DIRECTUS_EXTENSIONS_RATE_LIMITER_STORE | unset | Request and failed-OTP limiter store; otherwise uses SYNCHRONIZATION_STORE. | | REDIS_ENABLED | false | Enables component-based Redis configuration. | | REDIS | Directus setting | Complete URL; takes precedence over components. | | REDIS_HOST, REDIS_PORT, REDIS_USERNAME, REDIS_PASSWORD | unset | Required together when building a URL. | | MAGIC_LINKS_TOKEN_SECRET | Directus SECRET fallback | HMAC secret for token digests. | | MAGIC_LINKS_TOKEN_TTL | 15m | Token lifetime (ms, s, m, h, d, or w). | | MAGIC_LINKS_REQUEST_RATE_LIMIT | 5 | Requests per IP per 60 seconds for the request endpoint. | | MAGIC_LINKS_REDIRECT_URL_ALLOWLIST | required | Non-empty array of HTTP(S) URLs without credentials; explicit ports are allowed. | | MAGIC_LINKS_TOKEN_QUERY_PARAMETER | token | Query parameter used for the raw token. | | MAGIC_LINKS_COLLECTION | magic_links | Magic-link collection name. | | MAGIC_LINKS_EMAIL_TEMPLATE | magic-link | Directus Liquid template name. | | MAGIC_LINKS_EMAIL_SUBJECT | Your sign-in link | Optional email subject override. | | MAGIC_LINKS_EMAIL_PREVIEW_TEXT | Use this secure link to sign in to your account. | Optional inbox preview-text override. | | MAGIC_LINKS_EMAIL_REPLY_TO | unset | Optional reply-to email address. | | MAGIC_LINKS_EMAIL_SENDER | unset | Optional sender passed to the mail service. | | USE_MAGIC_LINK_CLEANUP | false | Enable scheduled cleanup. | | MAGIC_LINK_CLEANUP_WINDOW | 24h | Retention grace period after expiry or redemption. | | MAGIC_LINK_CLEANUP_CRON | */15 * * * * | Directus schedule expression for cleanup. |

Example:

MAGIC_LINKS_ENABLED=true
MAGIC_LINKS_REDIRECT_URL_ALLOWLIST=array:https://app.example.com/auth/magic-link
MAGIC_LINKS_TOKEN_TTL=15m
MAGIC_LINKS_REQUEST_RATE_LIMIT=5
USE_MAGIC_LINK_CLEANUP=true
MAGIC_LINK_CLEANUP_WINDOW=7d
MAGIC_LINK_CLEANUP_CRON=0 * * * *

The bundle uses Directus's internal MailService and thus accepts all of Directus's email transports: sendmail, smtp, mailgun, and ses. SMTP requires EMAIL_SMTP_HOST; its port, credentials, and other options are owned by Directus and the consumer. Mailgun requires its API key and domain; SES requires its access key ID, secret access key, and region. The bundle validates the selected transport before registering its endpoint.

Schema setup

When schema changes are enabled, the startup hook creates the configured MAGIC_LINKS_COLLECTION collection (default: magic_links), fields, and relation from the package's exported schema data. Existing compatible schema resources are preserved. Set DIRECTUS_EXTENSIONS_SCHEMA_CHANGES_ENABLED=false to disable schema changes globally, or MAGIC_LINKS_SCHEMA_CHANGES_ENABLED=false to disable only this bundle. Schema setup always uses a lock to prevent concurrent modifications; configure DIRECTUS_EXTENSIONS_LOCK_PROVIDER for multi-process deployments.

The magic-link record stores a required relation to directus_users; the related user's current email is used for delivery and is not duplicated in the magic-links table.

Scheduled cleanup

Set USE_MAGIC_LINK_CLEANUP=true to register the cleanup Cron job. Each run deletes records whose expires_at or redeemed_at is older than MAGIC_LINK_CLEANUP_WINDOW; the default retention window is 24 hours. For example, with MAGIC_LINK_CLEANUP_WINDOW=24h, a link that expired at 10:00 is eligible for deletion after 10:00 the following day. A link is also eligible after its redeemed_at timestamp has passed the same window. Pending, unexpired links are not deleted.

The schedule is registered by the hook entry only when MAGIC_LINKS_ENABLED and USE_MAGIC_LINK_CLEANUP are both enabled. Cleanup runs in a database transaction and logs its deleted count or failure without affecting request or redemption endpoints. In a multi-instance deployment that uses a process local SYNCHRONIZATION_STORE, each Directus process may run the schedule; concurrent cleanup runs are safe and idempotent, but operators should coordinate scheduling if duplicate executions are undesirable. Leave the feature disabled when another system owns retention for the configured magic-links collection.

Request endpoint

POST /auth/magic-links/request accepts:

{
  "email": "[email protected]",
  "redirectUrl": "https://app.example.com/auth/magic-link"
}

The email is trimmed and lowercased for lookup. redirectUrl must exactly match the configured allowlist; credentials, unsupported schemes, and unconfigured paths are rejected. HTTP URLs and explicit ports are supported for local development, but production deployments should use HTTPS. Invalid payloads and redirects return a Directus InvalidPayloadError.

For valid requests the endpoint always returns 202, regardless of whether an active local-provider user exists:

{
  "message": "If an account exists for this email address, a sign-in link has been sent."
}

The request endpoint applies a separate per-IP limit of 5 requests per minute by default, configured with MAGIC_LINKS_REQUEST_RATE_LIMIT.

The link uses a 256-bit random token, added as a query parameter to the provided redirectUrl. The parameter that is used is configurable with MAGIC_LINKS_TOKEN_QUERY_PARAMETER (default is token). Only the token's HMAC-SHA-256 digest is stored in token_hash; raw tokens are included only in the email URL and are never logged or persisted. Existing links remain valid until expiry or redemption. After the link transaction commits, email delivery starts in the background so SMTP or other transport latency does not affect the generic response. Delivery records transition from pending to sent or error, keeping failures auditable without changing the response. This is deliberately fire-and-forget: a process shutdown can leave a record pending or interrupt an in-flight delivery. Request another link when delivery fails.

Copy templates/magic-link.liquid into the configured EMAIL_TEMPLATES_PATH before enabling delivery. The repository's local and E2E Compose stacks mount this bundle directory automatically at /directus/templates.

The template receives url, email, expires_at, issued_at, ip, and user_agent, alongside Directus project variables. The included template renders a human-readable expiry, a clickable URL, and a neutral request-metadata callout. MailService accepts the subject, but does not have a separate preview-text metadata field; preview text is rendered by the Liquid template. If you use a custom template, render {{ preview_text }} near the start of the HTML body to preserve the preheader. Configure EMAIL_FROM and SMTP through Directus; the optional MAGIC_LINKS_EMAIL_REPLY_TO and MAGIC_LINKS_EMAIL_SENDER values are passed to Directus's MailService.

Redeem endpoint

POST /auth/magic-links/redeem accepts:

{
  "token": "raw-token-from-email",
  "otp": "123456",
  "mode": "json"
}

token is required. otp is required when the user has a configured personal TFA secret. mode defaults to json and accepts json, cookie, or session. The token is HMAC-digested and checked inside a transaction with a row lock. The link must be unexpired, unredeemed, active, and associated with Directus's default local provider.

Session modes mirror Directus login: json returns access and refresh tokens, cookie returns the access token and sets the refresh token in an HttpOnly cookie, and session sets the stateful session token in an HttpOnly cookie. Cookie names, TTLs, domain, Secure, and SameSite settings come from Directus's REFRESH_TOKEN_COOKIE_* and SESSION_COOKIE_* environment options.

Successful redemption validates a configured personal TFA secret, bootstraps a short-lived Directus session, and uses AuthenticationService.refresh() to issue Directus's normal authentication result before marking the link redeemed in the same transaction. For users without a personal TFA secret, the access-token JWT preserves Directus's role-policy enforce_tfa claim, so consumers can route the user into their TFA setup flow. This matches Directus 12.2 login behavior: only policies attached to the user's directly assigned role are considered for this claim (There is an open issue for this behaviour.

Invalid, expired, already redeemed, inactive, unsupported-provider, missing-OTP, and invalid-OTP requests return Directus's InvalidOtpError or generic credentials error as appropriate. When auth_login_attempts is configured, missing or invalid OTP attempts consume a per-user budget; successful redemption clears that user's budget. Requesting a new magic link does not reset the budget. The budget uses the same maximum as Directus login attempts and expires with the configured magic-link lifetime. OTP failures roll back the transaction, so the link can be retried until the budget is exhausted; other failed authentication leaves the link unredeemed as well. Set DIRECTUS_EXTENSIONS_RATE_LIMITER_STORE=redis and configure Directus's resolved Redis configuration for coordination across Directus replicas. A complete REDIS URL takes precedence over component values; component configuration requires REDIS_ENABLED=true (or SYNCHRONIZATION_STORE=redis) and all four Redis component variables. auth_login_attempts=null disables this limiter.

When the per-user budget is exhausted, Directus returns its standard HitRateLimitError response with HTTP status 429; stop retrying the user's links until the budget expires or use the application's normal sign-in flow.

When enforce_tfa is true, decode the access-token JWT payload client-side and route the authenticated user into the application's TFA setup flow. JWT decoding is only a UI/navigation hint; the server remains authoritative for authentication and OTP validation. The JWT can be decoded without the Directus signing secret, but must not be treated as trusted input for authorization.

The redemption limiter is separate from Directus's account-suspension behavior: it bounds OTP attempts per user and does not suspend the user. Apply rate limiting to both public routes at the edge or API gateway as an additional deployment control.

Clients should remove the token from the browser URL immediately after reading it, avoid analytics and application logs containing the token, and follow Directus's normal rules for storing returned refresh credentials. The endpoint does not modify Data Studio authentication.

The schema data is also available at:

import schema from '@onderwijsin/directus-magic-links-bundle/schema'

Boundaries

This extension is non-sandboxed, so it does not carry the trust required for Directus Marketplace distribution. Install it as an npm package in the Directus runtime. Its startup hook creates or reconciles the configured magic-links collection, fields, and relation; it creates no roles or policies and does not modify the Directus Data Studio authentication flow. The endpoint accepts public request and redeem calls, but the configured magic-links collection must remain private and must not be exposed through public CRUD permissions.

ConfigurevCORS for the frontend origin. Cookie and session modes require the deployment's normal CSRF protections because the browser sends the refresh or session cookie automatically.

For the rationale and security boundaries behind these choices, see the repository decision record: Magic-link architecture and security boundaries.

Studio Docs

The bundle seeds the Magic Links article from docs/magic-links.json when Studio Docs and data seeding are enabled. Set MAGIC_LINKS_DOCS_SEED_ENABLED=false to opt out.

The bundled documentation is available in Dutch. To translate it, keep the seeding strategy on versioning, start the extension with seeding enabled, edit and publish your translation on the main article, and reject incoming updates. Create a fresh translation from the Dutch source when the documentation changes.