Authentication
Sep 10, 2025

Real-time passwordless authentication flows with Supabase and Scalekit

Hrishikesh Premkumar
Founding Architect

Why passwordless authentication matters for modern apps

High-growth SaaS products often lose users at the login screen. A new user signs up, sees a password field, and drops off because they don’t want to manage yet another credential. At scale, even a small drop-off in onboarding translates into thousands of lost accounts.

Security adds more weight to the problem. Password databases are prime targets for breaches, reset flows are noisy and expensive to maintain, and compliance teams keep raising the bar. For developers, passwords create friction for users and risk for companies.

Passwordless authentication solves both issues. Instead of asking users to remember credentials, authentication happens through time-sensitive tokens, magic links, or one-time codes. Scalekit provides these flows out of the box, handling OTPs, email delivery, verification, and session management. Supabase complements this by storing user data securely, enforcing access rules, and powering realtime updates.

This guide walks through how to combine the two platforms into a complete passwordless system. Each section covers a critical part of the stack, from database schema and RLS to triggers, realtime events, and backup strategies.

Establishing the profiles table as the source of truth

Even though Scalekit manages passwordless authentication, the application still needs a persistent user record inside Supabase. Scalekit verifies the user’s identity, but Supabase is where application logic, row-level security, and realtime features operate. Without a proper schema, downstream policies and triggers cannot reliably attach to user sessions.

The profiles table serves as this bridge:

  • Identity anchor: Each user is assigned a UUID that serves as the stable primary key across the app.
  • Email mapping: Since Scalekit sessions are scoped to an email, the table must enforce unique email constraints.
  • Metadata storage: Profile data, preferences, and future extensibility live here, not in Scalekit.
  • Auditability: Created/updated timestamps are crucial for debugging and compliance.
  • Realtime hooks: Updates to this table drive subscriptions and broadcasts that keep the frontend in sync.

By letting Scalekit handle authentication and Supabase handle persistence, the architecture combines the strengths of both systems: secure passwordless auth flows with a strongly enforced, queryable data model.

The next section details the schema design for the profiles table, including required fields and recommended constraints.

Scalekit authentication integration with Supabase profiles

Scalekit provides the passwordless login layer, while Supabase serves as the persistent user store. Scalekit issues OTPs or magic links, verifies them, and the backend establishes a secure session. Supabase is used only to store and query user profiles after authentication. This separation ensures clean boundaries: Scalekit for identity, Supabase for data.

How the flow works

  1. Email submission
    • The React client calls your backend endpoint /api/auth/passwordless/send.
    • Backend uses scalekit.passwordless.sendPasswordlessEmail() to trigger OTP or magic link delivery.
  2. User verification
    • The client submits the OTP (verifyPasswordlessEmail) or hits the magic link callback.
    • Backend validates with Scalekit, returning a verified email.
  3. Session creation
    • Backend generates a JWT or HTTP-only cookie scoped to the email.
    • No Supabase Auth tokens are involved.
  4. Profile sync
    • Backend checks public.profiles in Supabase.
    • If no record exists, it inserts a new one with the email.
    • Updates timestamps or metadata as needed.
  5. Realtime broadcast
    • After sync, the backend emits a profile_sync event via the Supabase realtime channel.
    • This ensures clients immediately reflect the new login state.

Example: sending a passwordless email (backend)

// passwordless.ts
import { Scalekit } from "@scalekit-sdk/node";

const scalekit = new Scalekit(
  process.env.SCALEKIT_URL!,
  process.env.SCALEKIT_CLIENT_ID!,
  process.env.SCALEKIT_CLIENT_SECRET!
);

export async function sendLoginEmail(email: string) {
  return scalekit.passwordless.sendPasswordlessEmail(email, {
    template: "SIGNIN",
    expiresIn: 300,
    magiclinkAuthUri: `${process.env.APP_BASE_URL}/passwordless/verify`
  });
}

This integration keeps Scalekit responsible for authentication and Supabase focused on secure data access. Developers gain the flexibility of OTP or magic links without locking into Supabase Auth, while still leveraging Supabase’s strong RLS and realtime APIs.

Passwordless authentication flow

During our holiday sale, users want to quickly sign in without remembering passwords, while we need secure verification and seamless session handling across devices. Scalekit’s passwordless authentication handles OTPs or magic links, and Supabase stores persistent profiles securely.

Backend flow

Passwordless authentication flow

User submits email → Scalekit sends OTP/magic link → Backend verifies → Supabase syncs profile → Client receives session and realtime updates.

1. Sending OTP / Magic link

// server/routes/passwordless.ts import { Scalekit } from "@scalekit-sdk/node"; import { setSessionCookie } from '../session'; import { supabaseAdmin } from '../supabaseAdminClient'; const scalekit = new Scalekit( process.env.SCALEKIT_URL!, process.env.SCALEKIT_CLIENT_ID!, process.env.SCALEKIT_CLIENT_SECRET! ); export async function sendLoginEmail(email: string) { return scalekit.passwordless.sendPasswordlessEmail(email, { template: "SIGNIN", expiresIn: 300, magiclinkAuthUri: `${process.env.APP_BASE_URL}/passwordless/verify` }); }

2. Verification & profile sync

// server/routes/passwordless.ts router.post('/verify', async (req, res) => { const { authRequestId, code, linkToken } = req.body; const verifyResponse = await scalekit.passwordless.verifyPasswordlessEmail( { code, linkToken }, authRequestId ); const { email } = verifyResponse; // Sync profile in Supabase const { data: profile } = await supabaseAdmin .from('profiles') .upsert({ email }, { onConflict: 'email' }) .select() .single(); // Set session cookie setSessionCookie(res, email); // Broadcast Realtime update await supabaseAdmin.channel('broadcast').send({ type: 'profile_sync', email }); res.json({ profile }); });

Client flow

  • EmailForm: Collect user email and trigger OTP/magic link
  • OtpForm: Input OTP and verify
  • MagicLinkHandler: Automatically verify the magic link from the URL
// usePasswordlessAuth.ts export function usePasswordlessAuth() { const [loading, setLoading] = useState(false); const sendEmail = async (email: string) => { ... } const verify = async (authRequestId: string, code?: string, linkToken?: string) => { ... } return { sendEmail, verify, loading }; }

Why this matters

  • Fast, passwordless login during peak traffic events
  • Centralized session control via server-managed JWT cookies
  • Seamless profile sync with Realtime updates
  • Secure: Scalekit handles OTP/magic link verification, preventing password leaks or brute-force attacks

Optional enhancements

  • Edge functions: Handle verification callbacks, lightweight server computations, notifications
  • Database triggers: Auto-update last_login, audit logs, or notify external systems

Enforcing row-level security for email-scoped access

Imagine our e-commerce platform from the intro, during a holiday sale, thousands of users are logging in at once. We want to ensure that each user only sees their own profile and data, even under high load. Since authentication is handled by Scalekit, Supabase needs RLS policies to enforce this isolation.

Key design points

  • Email-based enforcement: Each row in public.profiles is tied to a unique email. Only the user whose email matches the JWT claim can access that row.
  • Client restrictions: Clients cannot insert or update arbitrary rows; they interact only with their own profile.
  • Scoped policies: Reads and updates check that the profiles.email matches the email claim in the server-issued session JWT. This ensures that even if a malicious client tries to fetch others’ data, Supabase denies access.

Example RLS policies for public.profiles

-- Enable RLS alter table public.profiles enable row level security; -- Read own profile create policy "Users can view their own profile" on public.profiles for select using (email = current_setting('request.jwt.claim.email')); -- Update own profile create policy "Users can update their own profile" on public.profiles for update using (email = current_setting('request.jwt.claim.email')) with check (email = current_setting('request.jwt.claim.email'));

Service role bypass

The backend uses Supabase’s service_role key when syncing profiles after Scalekit verification. This bypasses RLS so that inserts and updates always succeed, even if no row exists yet. Clients never see this key, they are limited to their own records by RLS.

By linking this back to our e-commerce scenario, RLS ensures that during high-traffic periods:

  • Users can only access their own profiles.
  • No cross-user data leaks occur, even if someone tries to manipulate requests.
  • The system scales securely, letting your backend handle high concurrency without compromising user isolation.

Realtime updates for user profiles

During our holiday sale scenario, imagine a user updating their profile or preferences from one device while browsing on another. To provide a seamless experience, we want changes to propagate instantly without forcing a full page refresh. This is where Supabase Realtime comes in.

How it works

  1. Postgres changes feed: Any insert or update to public.profiles triggers a change event.
  2. Client subscription: Each client subscribes only to the row corresponding to their email, ensuring they receive updates for their profile only.
  3. Broadcast fallback: After a profile sync on the server (post-Scalekit verification), a broadcast event ensures that any missed updates are delivered, even if the client temporarily loses connection.
Realtime updates and broadcast flow

Profile changes in Supabase are pushed to clients via row-level subscriptions and broadcast channels, ensuring instant updates across devices.

Implementation example

// hooks/useUserRealtime.ts import { useEffect, useState } from 'react'; import { supabase } from '../supabaseClient'; import { Profile } from '../types'; export function useUserRealtime(email: string) { const [profile, setProfile] = useState<Profile | null>(null); useEffect(() => { const channel = supabase .channel('public:profiles') .on( 'postgres_changes', { event: '*', schema: 'public', table: 'profiles', filter: `email=eq.${email}` }, (payload) => { setProfile(payload.new); } ) .subscribe(); return () => { supabase.removeChannel(channel); }; }, [email]); return profile; }

Server-side broadcast

// After Scalekit verification supabaseAdmin .from('profiles') .update({ last_verified: new Date() }) .eq('email', email); supabaseAdmin .channel('broadcast') .send({ type: 'profile_sync', email });

Why this matters

  • Users instantly see updates made from other devices.
  • During high concurrency (like our holiday sale), we avoid stale data or race conditions.
  • Combined with RLS, it guarantees that only the intended user receives their own profile changes.

By linking Realtime updates with Scalekit authentication and RLS, we create a secure, seamless, and user-centric experience, even under heavy load.

Passwordless authentication with Scalekit

Recall our holiday sale scenario: users want to quickly sign in without remembering passwords, while we need secure verification and seamless session handling across devices. Scalekit passwordless authentication solves this perfectly.

Flow overview

  1. User enters email → request is sent to our backend.
  2. Backend calls Scalekit → sends a verification code or magic link.
  3. User verifies → backend confirms with Scalekit, creates a session cookie, and syncs the profile in Supabase.
  4. Realtime update → client receives any profile updates via the Realtime subscription.

Backend: Sending a passwordless email

// server/routes/passwordless.ts import { Router } from 'express'; import { scalekit } from '../scalekitClient'; import { setSessionCookie } from '../session'; const router = Router(); router.post('/send', async (req, res) => { const { email } = req.body; try { const sendResponse = await scalekit.passwordless.sendPasswordlessEmail(email, { template: 'SIGNIN', expiresIn: 300, magiclinkAuthUri: `${process.env.APP_BASE_URL}/passwordless/verify`, }); res.json({ authRequestId: sendResponse.authRequestId }); } catch (error) { res.status(400).json({ error: error.message }); } }); export default router;

Backend: Verifying the code or magic link

router.post('/verify', async (req, res) => { const { authRequestId, code, linkToken } = req.body; try { const verifyResponse = await scalekit.passwordless.verifyPasswordlessEmail( { code, linkToken }, authRequestId ); // Sync profile in Supabase const { email } = verifyResponse; await supabaseAdmin .from('profiles') .upsert({ email }, { onConflict: 'email' }); // Set session cookie setSessionCookie(res, email); // Broadcast Realtime update await supabaseAdmin.channel('broadcast').send({ type: 'profile_sync', email }); res.json({ email }); } catch (error) { res.status(400).json({ error: error.message }); } });

Client-side hooks

// usePasswordlessAuth.ts import { useState } from 'react'; import axios from 'axios'; export function usePasswordlessAuth() { const [loading, setLoading] = useState(false); const sendEmail = async (email: string) => { setLoading(true); const { data } = await axios.post('/api/auth/passwordless/send', { email }); setLoading(false); return data.authRequestId; }; const verify = async (authRequestId: string, code?: string, linkToken?: string) => { setLoading(true); const { data } = await axios.post('/api/auth/passwordless/verify', { authRequestId, code, linkToken }); setLoading(false); return data.email; }; return { sendEmail, verify, loading }; }

Why this matters

  • Fast, passwordless login for users during high-traffic events.
  • Centralized session control, our server manages JWT cookies, not Supabase Auth.
  • Seamless profile sync across devices with Realtime events.
  • Security and flexibility, Scalekit handles OTP/magic link verification, preventing password leaks or brute force attacks.

By integrating Scalekit into our backend flow, we eliminate the need for Supabase Auth entirely while still leveraging Supabase for secure, real-time data storage and access. Users get a smooth, instant experience, and developers retain full control over sessions and data sync.

Database schema and Row-Level Security (RLS)

Imagine our holiday sale scenario: thousands of users are logging in simultaneously, redeeming offers, and updating their profiles. To handle this smoothly, we need a database design that ensures each user sees only their own data, while supporting real-time updates without overloading the server.

Profiles table

create table public.profiles ( id uuid primary key default gen_random_uuid(), email text unique not null, name text, last_login timestamptz default now(), created_at timestamptz default now(), updated_at timestamptz default now() );

Why this table matters:

  • Each user has a unique record, keyed by their email, linking Scalekit authentication to Supabase profiles.
  • Stores essential profile data like name and last_login, useful for tracking activity during high-traffic periods.
  • Enables the backend to safely upsert verified emails from Scalekit.
  • Supports Realtime notifications so the frontend reflects profile changes instantly.

Row-Level Security (RLS)

To ensure strict user isolation even under heavy load:

-- Enable RLS alter table public.profiles enable row level security; -- Select own profile create policy select_own_profile on public.profiles for select using (email = current_setting('jwt.claims.email')); -- Update own profile create policy update_own_profile on public.profiles for update using (email = current_setting('jwt.claims.email')); -- Insert own profile create policy insert_own_profile on public.profiles for insert with check (email = current_setting('jwt.claims.email'));

Key points:

  • JWTs issued by the backend after Scalekit verification provide the email claim for policy enforcement.
  • Clients can only access or modify their own profiles.
  • The server uses Supabase’s service-role key to bypass RLS for safe profile inserts/updates.

Real-Time updates

During our sale, a user might log in from multiple devices or update their preferences. To keep the UI in sync:

  • Clients subscribe to their own profile rows using email-scoped filters.
  • Any change in public.profiles triggers a Realtime event.
  • Broadcast fallbacks ensure updates reach clients even if they temporarily disconnect.

Why this matters in our scenario:

  • Users instantly see updated profile info and session states.
  • RLS ensures no cross-user data leaks, even during traffic spikes.
  • Combines secure isolation with seamless frontend updates, making the holiday sale experience smooth for both users and developers.

Optional enhancements: Database triggers

While our current setup relies on server-side sync for profile updates, you could also add Postgres triggers to automate actions on insert/update events. For example:

  • Automatically update last_login on user verification.
  • Send audit logs to a separate table for compliance.
  • Notify external systems of profile changes.

Triggers can complement Realtime subscriptions and ensure server-side logic remains consistent even if multiple services interact with the database.

Realtime subscriptions & broadcast

During our simulated holiday sale, hundreds or thousands of users may log in at the same time. To ensure the UI reflects the latest profile state without overloading the server, we leverage Supabase Realtime combined with a broadcast fallback.

How it works

1. Client-side subscription:

import { createClient } from '@supabase/supabase-js'; const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY); const { data: subscription } = supabase .from(`public.profiles:email=eq.${userEmail}`) .on('UPDATE', payload => { console.log('Profile updated:', payload.new); // Update client state here }) .subscribe();
  • Each client subscribes to their own profile using the email filter, enforced by RLS.
  • Any changes (like session creation, preference updates, or verification status) trigger an immediate event.

2. Server-side broadcast:

import { supabaseAdmin } from './supabaseAdminClient'; const broadcastProfileUpdate = async (profile) => { await supabaseAdmin .from('broadcast') .insert({ event: 'profile_sync', payload: profile }); };
  • After Scalekit verification, the server upserts the profile and triggers a broadcast event.
  • This ensures the client receives updates even if RLS or subscription latency prevents direct realtime changes.

Why it matters

  • Instant UI feedback: Users immediately see their updated verification status, preferences, or session info.
  • Scalable: Even with hundreds of concurrent logins, the broadcast channel ensures reliable delivery without heavy polling.
  • Secure: RLS and email-scoped subscriptions prevent exposure of other users’ data.

Connecting to our story

Imagine the peak of a holiday sale: multiple users are logging in simultaneously, redeeming offers, and updating preferences. Without Realtime updates, some clients might see stale session states, causing frustration or errors. By combining email-scoped subscriptions with server broadcasts, every user sees their latest profile instantly, while the backend safely manages thousands of concurrent events.

Server implementation: Passwordless flow

With Scalekit handling all authentication, the server’s main responsibilities are:

  1. Initiating the passwordless flow
  2. Verifying users via OTP or magic link
  3. Syncing profiles to Supabase
  4. Creating session cookies for secure client access

This ensures that even during our holiday sale spike, every login is fast, reliable, and secure.

1. Sending the verification email

When a user enters their email, the backend calls Scalekit to send either a verification code or a magic link:

// server/routes/passwordless.ts import scalekit from '../scalekitClient'; app.post('/api/auth/passwordless/send', async (req, res) => { const { email } = req.body; try { const sendResponse = await scalekit.passwordless.sendPasswordlessEmail(email, { template: 'SIGNIN', magiclinkAuthUri: `${APP_BASE_URL}/passwordless/verify`, expiresIn: 300, }); res.json(sendResponse); } catch (error) { res.status(500).json({ error: 'Failed to send verification email' }); } });
  • The server never stores passwords. Scalekit handles verification securely.
  • Response includes authRequestId, which is required for verification.

2. Verifying the user

After receiving the OTP or clicking the magic link, the server validates it with Scalekit:

app.post('/api/auth/passwordless/verify', async (req, res) => { const { code, linkToken, authRequestId } = req.body; const verifyResponse = await scalekit.passwordless.verifyPasswordlessEmail( code ? { code } : { linkToken }, authRequestId ); const email = verifyResponse.email; // Sync profile in Supabase const { data: profile } = await supabaseAdmin .from('public.profiles') .upsert({ email }, { onConflict: 'email' }) .select() .single(); // Create session cookie const sessionToken = createSessionCookie(email); res.cookie('session', sessionToken, { httpOnly: true, secure: true }); res.json({ profile }); });
  • Uses the service role client to bypass RLS for trusted profile sync.
  • Creates a JWT session cookie scoped to the email.

3. Real-time profile broadcast

After syncing the profile, the server triggers a broadcast event so the client subscription updates immediately:

await supabaseAdmin.from('broadcast').insert({ event: 'profile_sync', payload: profile });

Why this setup is ideal

  • Scalable: Server never handles passwords; Scalekit manages auth load.
  • Reliable: Profile sync and broadcasts guarantee the client sees the latest state instantly.
  • Secure: Email-scoped sessions + RLS ensure users only access their own data.

Connecting to our story

During the holiday sale, a user logs in with their email. Scalekit sends the verification link in seconds. The server validates it, updates the Supabase profile, and the client instantly sees their verified state, even in a surge of hundreds of logins. No stale UI, no race conditions, no downtime.

Optional: Edge Functions for Enhanced Workflows

Supabase Edge Functions could be used to offload certain backend tasks, such as:

  • Handling verification callbacks from Scalekit
  • Performing lightweight server-side computations before profile sync
  • Triggering notifications or custom webhooks
    Incorporating Edge Functions can reduce latency and scale specific operations independently from your main server.

Client implementation: Passwordless UI & hooks

On the client side, our goal is to make the login process fast, intuitive, and reactive, even when traffic spikes during a sale.

1. Collecting user email

The user enters their email in a simple React form. Upon submission, the request is sent to our backend:

// client/components/EmailForm.tsx import { useState } from 'react'; import usePasswordlessAuth from '../hooks/usePasswordlessAuth'; export default function EmailForm() { const [email, setEmail] = useState(''); const { sendVerificationEmail, status } = usePasswordlessAuth(); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); await sendVerificationEmail(email); }; return ( <form onSubmit={handleSubmit}> <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="Enter your email" required /> <button type="submit">Send Verification</button> {status && <p>{status}</p>} </form> ); }
  • usePasswordlessAuth handles all interactions with the server.
  • The form is minimal to reduce friction during high-traffic periods.

2. Verifying OTP or Magic link

Depending on the method the user chooses (OTP or magic link), we display the appropriate UI:

// client/components/OtpForm.tsx import { useState } from 'react'; import usePasswordlessAuth from '../hooks/usePasswordlessAuth'; export default function OtpForm({ authRequestId }: { authRequestId: string }) { const [code, setCode] = useState(''); const { verifyCode, status } = usePasswordlessAuth(); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); await verifyCode(code, authRequestId); }; return ( <form onSubmit={handleSubmit}> <input type="text" value={code} onChange={(e) => setCode(e.target.value)} placeholder="Enter OTP" required /> <button type="submit">Verify</button> {status && <p>{status}</p>} </form> ); }
  • Handles verification responses from the server.
  • Displays clear feedback for invalid or expired codes, critical when many users are logging in simultaneously.

3. Magic link handling

For magic links, we capture the link_token from the URL and trigger verification automatically:

// client/components/MagicLinkHandler.tsx import { useEffect } from 'react'; import { useNavigate, useSearchParams } from 'react-router-dom'; import usePasswordlessAuth from '../hooks/usePasswordlessAuth'; export default function MagicLinkHandler() { const [searchParams] = useSearchParams(); const navigate = useNavigate(); const { verifyMagicLink } = usePasswordlessAuth(); useEffect(() => { const token = searchParams.get('link_token'); if (token) { verifyMagicLink(token).then(() => navigate('/dashboard')); } }, [searchParams]); return <p>Verifying your login...</p>; }
  • Seamless UX: user clicks the link and is instantly logged in.
  • Scales reliably during peak login periods, like our holiday sale scenario.

4. Realtime profile updates

After verification, the client subscribes to profile changes using our useUserRealtime hook:

import { useEffect, useState } from 'react'; import { supabase } from '../supabaseClient'; export default function useUserRealtime(email: string) { const [profile, setProfile] = useState(null); useEffect(() => { const subscription = supabase .from(`public.profiles:email=eq.${email}`) .on('UPDATE', (payload) => setProfile(payload.new)) .subscribe(); return () => supabase.removeSubscription(subscription); }, [email]); return profile; }
  • Ensures the UI always reflects the latest user state, even if multiple devices are logged in.
  • Prevents stale profiles or race conditions during heavy traffic.

Linking back to the holiday sale story

During the sale, users logging in across devices see instant updates to their profiles and session state. Scalekit handles authentication at scale, and the frontend reacts immediately to profile changes, no lag, no dropped sessions.

Realtime updates & broadcast

Scenario: During our holiday sale, users log in from multiple devices, update preferences, or redeem offers. We need a system that instantly reflects changes without stale UI, even under high traffic.

How it works:

Client-side subscription:

// hooks/useUserRealtime.ts import { useEffect, useState } from 'react'; import { supabase } from '../supabaseClient'; import { Profile } from '../types'; export function useUserRealtime(email: string) { const [profile, setProfile] = useState<Profile | null>(null); useEffect(() => { const channel = supabase .channel('public:profiles') .on( 'postgres_changes', { event: '*', schema: 'public', table: 'profiles', filter: `email=eq.${email}` }, (payload) => setProfile(payload.new) ) .subscribe(); return () => supabase.removeChannel(channel); }, [email]); return profile; }

Server-side broadcast:

// After Scalekit verification and profile sync await supabaseAdmin .from('broadcast') .send({ type: 'profile_sync', email });

Why this matters:

  • Instant UI updates: Users immediately see profile changes across devices.
  • Concurrency-safe: Even hundreds of simultaneous logins are handled seamlessly.
  • Secure: RLS and email-scoped subscriptions prevent exposure of other users’ data.

Optional: Backup and disaster recovery

For production apps, consider implementing regular backups of the public.profiles table and other critical data. Supabase provides built-in backups, but additional strategies could include:

  • Periodic exports to cloud storage
  • Versioned backups of audit logs
  • Automated restore tests to ensure reliability

These strategies help maintain data integrity during high-traffic events or unexpected incidents, complementing the secure auth and realtime sync flow described above.

Conclusion

In this guide, we implemented a passwordless authentication flow using Scalekit for secure email OTPs and magic links, while relying on Supabase purely as a database with realtime updates. This approach eliminates the need for traditional auth providers, letting your server manage JWT sessions scoped to user emails. With Realtime subscriptions and a service-role sync, the frontend stays instantly updated, ensuring a seamless experience for both the user and the developer managing the backend.

Looking back at our initial scenario, the developer trying to handle auth, session state, and profile updates efficiently, this setup directly addresses those pain points. Scalekit offloads the complexity of verification, email sending, and token management, while Supabase handles persistent data storage and RLS-based security. The combination ensures a clean separation of concerns and reduces potential bugs related to session handling or realtime updates.

The architecture we’ve built is both secure and scalable. Using JWTs scoped to email keeps session management simple, service-role access allows safe server-side updates bypassing RLS, and realtime subscriptions keep clients in sync without heavy polling. Developers can extend this foundation with custom RLS policies, additional tables, or frontend state management patterns without disrupting the auth flow.

As for next steps, you can explore advanced Scalekit features like multi-factor authentication, suspicious activity monitoring, or custom email templates. On the database side, experimenting with Supabase RLS rules, triggers, and additional realtime channels can further enhance your app. Finally, reviewing the Scalekit and Supabase documentation will give you deeper insights and open avenues to build a fully production-ready passwordless system tailored to your specific app needs.

FAQ

How does Scalekit handle OTP expiration and rate limits?

Scalekit enforces a default 5-attempt limit per 10-minute window for OTP verification to prevent brute-force attacks. Expired codes are rejected automatically, and new requests trigger a fresh auth_request_id. Rate limiting is configurable on the Scalekit dashboard for production-grade security.

Can Scalekit magic links be used across devices?

Yes, but only if same browser origin enforcement is disabled. If enabled, the magic link must be opened in the original browser session to prevent token hijacking, ensuring a secure passwordless authentication flow.

Why use service-role Supabase client for profile sync?

The service-role client bypasses RLS, allowing the server to safely insert/update public.profiles without exposing sensitive operations to the client. This ensures secure server-side sync while preserving fine-grained access control for frontend queries.

How does Postgres Realtime integrate with JWT-based sessions?

Realtime subscriptions listen to public.profiles changes, but JWT-scoped sessions prevent unauthorized access. By combining RLS policies and JWT validation, developers can achieve instant frontend updates without exposing sensitive data or service-role credentials.

Can this passwordless system scale for multi-tenant apps?

Yes, by extending the public.profiles schema with tenant_id and adjusting RLS policies, each tenant’s data can remain isolated. Coupled with Scalekit’s stateless auth and Supabase Realtime, the system supports multi-tenant passwordless workflows with minimal server overhead.

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